34import * as
fs from
'fs/promises';
35import * as
path from
'path';
36import * as
os from
'os';
37import { RandomLogo,
logo } from
'../modules/randomLogo';
44 private _html:
string =
'';
45 private messageHandlers: Array<(message: any) =>
void> = [];
59 set html(value:
string) {
69 this.messageHandlers.push(handler);
70 return { dispose: () => { } };
78 this.messageHandlers.forEach(handler => handler(message));
103 public viewType:
string,
104 public title:
string,
105 public showOptions:
vscode.ViewColumn,
106 public options:
vscode.WebviewPanelOptions &
vscode.WebviewOptions
116suite(
'RandomLogo Test Suite', () => {
120 let originalCreateWebviewPanel: typeof
vscode.window.createWebviewPanel;
131 tempDir = await
fs.mkdtemp(
path.join(
os.tmpdir(),
'randomlogo-test-'));
132 logoDir =
path.join(tempDir,
'logos');
133 subDir =
path.join(logoDir,
'subdir');
135 await
fs.mkdir(logoDir, { recursive:
true });
136 await
fs.mkdir(subDir, { recursive:
true });
141 name:
'simple-logo.txt',
142 content:
' **** \n * * \n * * \n **** '
145 name:
'complex-logo.txt',
146 content:
'╔══════════════════════════════════════╗\n║ LOGO TITLE ║\n║ ║\n║ ██████ ██████ ███ ███ ██████ ║\n║ ██ ██ ██ ████ ████ ██ ██ ║\n║ ██ ██ ██ ██ ████ ██ ██████ ║\n║ ██ ██ ██ ██ ██ ██ ██ ║\n║ ██████ ██████ ██ ██ ██ ║\n║ ║\n╚══════════════════════════════════════╝'
149 name:
'multiline-logo.txt',
150 content:
'Line 1\nLine 2\nLine 3\nLine 4\nLine 5'
154 const subDirLogoFiles = [
156 name:
'nested-logo.txt',
157 content:
' /\\_/\\ \n ( o.o ) \n > ^ < '
162 for (
const file of logoFiles) {
163 await
fs.writeFile(
path.join(logoDir, file.name), file.content,
'utf8');
167 for (
const file of subDirLogoFiles) {
168 await
fs.writeFile(
path.join(subDir, file.name), file.content,
'utf8');
172 await
fs.writeFile(
path.join(logoDir,
'readme.md'),
'# This is not a logo',
'utf8');
173 await
fs.writeFile(
path.join(logoDir,
'config.json'),
'{"test": true}',
'utf8');
176 originalCreateWebviewPanel =
vscode.window.createWebviewPanel;
177 vscode.window.createWebviewPanel = (
180 showOptions:
vscode.ViewColumn | { readonly viewColumn:
vscode.ViewColumn; readonly preserveFocus?:
boolean | undefined; },
181 options?:
vscode.WebviewPanelOptions &
vscode.WebviewOptions
183 const column = typeof showOptions ===
'object' ? showOptions.viewColumn : showOptions;
184 const opts = options || {};
186 lastCreatedWebview = lastCreatedPanel.webview;
190 Object.defineProperty(lastCreatedWebview,
'html', {
191 get: () => capturedHtml,
193 capturedHtml = value;
197 return lastCreatedPanel as any;
205 teardown(async () => {
207 if (originalCreateWebviewPanel) {
208 vscode.window.createWebviewPanel = originalCreateWebviewPanel;
213 await
fs.rm(tempDir, { recursive:
true, force:
true });
223 suite(
'Constructor and Initialization', () => {
228 test(
'should create instance with default parameters', () => {
237 test(
'should create instance with root directory parameter', async () => {
242 await
new Promise(resolve => setTimeout(resolve, 50));
249 test(
'should create instance with both root directory and working directory', async () => {
250 const randomLogo =
new RandomLogo(logoDir, tempDir);
254 await
new Promise(resolve => setTimeout(resolve, 50));
261 test(
'should handle undefined parameters gracefully', () => {
262 const randomLogo =
new RandomLogo(undefined, undefined);
271 suite(
'Directory and Path Management', () => {
276 test(
'should update root directory successfully', async () => {
278 const result = await
randomLogo.updateRootDir(logoDir);
279 assert.strictEqual(result,
true);
286 test(
'should handle invalid root directory gracefully', async () => {
288 const invalidPath =
path.join(tempDir,
'nonexistent');
289 const result = await
randomLogo.updateRootDir(invalidPath);
290 assert.strictEqual(result,
false);
297 test(
'should update current working directory successfully', () => {
299 const result =
randomLogo.updateCurrentWorkingDirectory(tempDir);
300 assert.strictEqual(result,
true);
307 test(
'should handle relative paths with working directory', () => {
309 randomLogo.updateCurrentWorkingDirectory(tempDir);
310 assert.strictEqual(
randomLogo.updateCurrentWorkingDirectory(
'./test'),
true);
318 suite(
'Logo File Discovery and Management', () => {
323 test(
'should discover logo files in root directory', async () => {
327 await
new Promise(resolve => setTimeout(resolve, 100));
336 assert.fail(
'Should have discovered logo files');
344 test(
'should discover logo files recursively in subdirectories', async () => {
348 await
new Promise(resolve => setTimeout(resolve, 100));
351 const logoFileNames:
string[] = [];
352 for (let i = 0; i < 10; i++) {
355 logoFileNames.push(
logo.fileName);
362 assert.ok(logoFileNames.length > 0,
'Should have found logo files');
363 assert.ok(logoFileNames.some(name => name.endsWith(
'.txt')),
'Should have .txt files');
370 test(
'should filter non-txt files correctly', async () => {
374 await
new Promise(resolve => setTimeout(resolve, 100));
377 for (let i = 0; i < 5; i++) {
380 assert.ok(
logo.fileName.endsWith(
'.txt'), `Logo file ${logo.fileName} should be a .txt file`);
391 test(
'should handle empty directories gracefully', async () => {
392 const emptyDir =
path.join(tempDir,
'empty');
393 await
fs.mkdir(emptyDir, { recursive:
true });
398 await
new Promise(resolve => setTimeout(resolve, 50));
402 assert.fail(
'Should throw error for empty directory');
404 assert.ok(error instanceof Error);
413 suite(
'Random Selection Algorithm', () => {
418 test(
'should select different logos on multiple calls', async () => {
422 await
new Promise(resolve => setTimeout(resolve, 100));
424 const selectedLogos =
new Set<string>();
427 for (let i = 0; i < 10; i++) {
430 selectedLogos.add(
logo.fileName);
437 assert.ok(selectedLogos.size > 0,
'Should select at least one logo');
444 test(
'should always return valid logo from dataset', async () => {
448 await
new Promise(resolve => setTimeout(resolve, 100));
450 for (let i = 0; i < 5; i++) {
454 assert.ok(
logo.fileName,
'Should have a filename');
455 assert.ok(
logo.fileName.endsWith(
'.txt'),
'Should be a .txt file');
456 assert.ok(Array.isArray(
logo.logoContent),
'Logo content should be an array');
457 assert.ok(
logo.logoContent.length > 0,
'Logo should have content lines');
468 test(
'should handle single logo file correctly', async () => {
470 const singleLogoDir =
path.join(tempDir,
'single');
471 await
fs.mkdir(singleLogoDir, { recursive:
true });
472 await
fs.writeFile(
path.join(singleLogoDir,
'only-logo.txt'),
'Single\nLogo\nContent',
'utf8');
474 const randomLogo =
new RandomLogo(singleLogoDir);
477 await
new Promise(resolve => setTimeout(resolve, 50));
481 assert.strictEqual(
logo.fileName,
'only-logo.txt');
482 assert.deepStrictEqual(
logo.logoContent, [
'Single',
'Logo',
'Content']);
489 test(
'should properly split logo content by lines', async () => {
493 await
new Promise(resolve => setTimeout(resolve, 100));
497 assert.ok(Array.isArray(
logo.logoContent),
'Logo content should be an array of lines');
500 const joinedContent =
logo.logoContent.join(
'\n');
501 assert.ok(joinedContent.length > 0,
'Content should not be empty');
509 suite(
'Error Handling and Edge Cases', () => {
514 test(
'should throw error when no root directory is set', async () => {
519 assert.fail(
'Should throw error when no root directory is set');
521 assert.ok(error instanceof Error);
529 test(
'should handle corrupted or empty logo files gracefully', async () => {
531 const emptyLogoDir =
path.join(tempDir,
'empty-files');
532 await
fs.mkdir(emptyLogoDir, { recursive:
true });
533 await
fs.writeFile(
path.join(emptyLogoDir,
'empty-logo.txt'),
'',
'utf8');
535 const randomLogo =
new RandomLogo(emptyLogoDir);
538 await
new Promise(resolve => setTimeout(resolve, 50));
542 assert.strictEqual(
logo.fileName,
'empty-logo.txt');
545 assert.strictEqual(
logo.logoContent.length, 1);
553 test(
'should handle permission errors during file discovery', async () => {
557 const result = await
randomLogo.updateRootDir(
'/nonexistent/path/that/should/not/exist');
558 assert.strictEqual(result,
false);
565 test(
'should handle various line ending formats', async () => {
567 const lineEndingDir =
path.join(tempDir,
'line-endings');
568 await
fs.mkdir(lineEndingDir, { recursive:
true });
570 await
fs.writeFile(
path.join(lineEndingDir,
'unix-endings.txt'),
'Line1\nLine2\nLine3',
'utf8');
571 await
fs.writeFile(
path.join(lineEndingDir,
'windows-endings.txt'),
'Line1\r\nLine2\r\nLine3',
'utf8');
573 const randomLogo =
new RandomLogo(lineEndingDir);
576 await
new Promise(resolve => setTimeout(resolve, 50));
578 for (let i = 0; i < 5; i++) {
582 assert.strictEqual(
logo.logoContent.length, 3);
583 assert.strictEqual(
logo.logoContent[0],
'Line1');
584 assert.strictEqual(
logo.logoContent[1],
'Line2');
585 assert.strictEqual(
logo.logoContent[2],
'Line3');
594 suite(
'HTML Content Generation', () => {
599 test(
'should generate valid HTML content for webview', async () => {
603 await
new Promise(resolve => setTimeout(resolve, 100));
607 assert.ok(capturedHtml,
'Should generate HTML content');
608 assert.ok(capturedHtml.includes(
'<!DOCTYPE html>'),
'Should be valid HTML');
609 assert.ok(capturedHtml.includes(
'<html lang="en">'),
'Should have language attribute');
610 assert.ok(capturedHtml.includes(
'<body>'),
'Should have body tag');
617 test(
'should include logo content in HTML', async () => {
621 await
new Promise(resolve => setTimeout(resolve, 100));
625 assert.ok(capturedHtml.includes(
'<pre id="ascii">'),
'Should include ASCII art container');
627 const preMatch = capturedHtml.match(/<pre
id=
"ascii">(.*?)<\/pre>/s);
628 assert.ok(preMatch,
'Should have ASCII art content');
629 assert.ok(preMatch[1].length > 0,
'ASCII art content should not be empty');
636 test(
'should include interactive buttons in HTML', async () => {
640 await
new Promise(resolve => setTimeout(resolve, 100));
644 assert.ok(capturedHtml.includes(
'id="copyBtn"'),
'Should include copy button');
645 assert.ok(capturedHtml.includes(
'id="zoomInBtn"'),
'Should include zoom in button');
646 assert.ok(capturedHtml.includes(
'id="zoomOutBtn"'),
'Should include zoom out button');
653 test(
'should include CSS styling', async () => {
657 await
new Promise(resolve => setTimeout(resolve, 100));
661 assert.ok(capturedHtml.includes(
'<style>'),
'Should include CSS styling');
662 assert.ok(capturedHtml.includes(
'font-family:'),
'Should have font family styling');
663 assert.ok(capturedHtml.includes(
'pre {'),
'Should have pre tag styling for ASCII art');
670 test(
'should include logo filename in display', async () => {
674 await
new Promise(resolve => setTimeout(resolve, 100));
680 capturedHtml.includes(
'.txt') ||
681 lastCreatedPanel.title.includes(
'.txt'),
682 'Should display logo filename'
690 test(
'should handle empty or undefined logo content gracefully', async () => {
692 const problematicDir =
path.join(tempDir,
'problematic');
693 await
fs.mkdir(problematicDir, { recursive:
true });
694 await
fs.writeFile(
path.join(problematicDir,
'empty.txt'),
'',
'utf8');
696 const randomLogo =
new RandomLogo(problematicDir);
699 await
new Promise(resolve => setTimeout(resolve, 50));
704 assert.ok(capturedHtml,
'Should generate HTML even with empty content');
705 assert.ok(capturedHtml.includes(
'<!DOCTYPE html>'),
'Should be valid HTML');
713 suite(
'JavaScript Functionality', () => {
718 test(
'should include copy button script', async () => {
722 await
new Promise(resolve => setTimeout(resolve, 100));
726 assert.ok(capturedHtml.includes(
"getElementById('copyBtn')"),
'Should include copy button script');
727 assert.ok(capturedHtml.includes(
'navigator.clipboard.writeText'),
'Should use clipboard API');
728 assert.ok(capturedHtml.includes(
"vscode.postMessage({ type: 'copied' })"),
'Should post message to VS Code');
735 test(
'should include zoom functionality script', async () => {
739 await
new Promise(resolve => setTimeout(resolve, 100));
743 assert.ok(capturedHtml.includes(
'updateFontSize'),
'Should include font size update function');
744 assert.ok(capturedHtml.includes(
"getElementById('zoomInBtn')"),
'Should include zoom in handler');
745 assert.ok(capturedHtml.includes(
"getElementById('zoomOutBtn')"),
'Should include zoom out handler');
746 assert.ok(capturedHtml.includes(
'currentSize'),
'Should track current font size');
753 test(
'should include VS Code API integration', async () => {
757 await
new Promise(resolve => setTimeout(resolve, 100));
761 assert.ok(capturedHtml.includes(
'acquireVsCodeApi()'),
'Should acquire VS Code API');
762 assert.ok(capturedHtml.includes(
'vscode.postMessage'),
'Should use VS Code message posting');
769 test(
'should implement font size constraints in zoom', async () => {
773 await
new Promise(resolve => setTimeout(resolve, 100));
778 assert.ok(capturedHtml.includes(
'>= 2'),
'Should have minimum font size constraint');
780 assert.ok(capturedHtml.includes(
'currentSize = 20'),
'Should initialize font size');
788 suite(
'Integration and Message Handling', () => {
793 test(
'should setup message handler for copy events', async () => {
797 await
new Promise(resolve => setTimeout(resolve, 100));
799 let messageReceived =
false;
800 let receivedMessage: any =
null;
803 const originalOnMessage = lastCreatedWebview?.onDidReceiveMessage;
804 if (lastCreatedWebview) {
805 lastCreatedWebview.onDidReceiveMessage = (handler: (message: any) => void) => {
817 assert.ok(lastCreatedPanel,
'Should create webview panel');
818 assert.ok(lastCreatedWebview,
'Should create webview');
825 test(
'should create webview with correct parameters', async () => {
829 await
new Promise(resolve => setTimeout(resolve, 100));
833 assert.ok(lastCreatedPanel,
'Should create webview panel');
834 assert.strictEqual(lastCreatedPanel.showOptions,
vscode.ViewColumn.One,
'Should use ViewColumn.One');
835 assert.ok(lastCreatedPanel.options.enableScripts,
'Should enable scripts');
836 assert.ok(lastCreatedPanel.title.endsWith(
'.txt'),
'Title should be the filename');
843 test(
'should handle message communication properly', async () => {
847 await
new Promise(resolve => setTimeout(resolve, 100));
852 if (lastCreatedWebview && lastCreatedWebview.postMessage) {
853 lastCreatedWebview.postMessage({
type:
'copied' });
857 assert.ok(
true,
'Message handling should not throw errors');
865 suite(
'Performance and Memory Management', () => {
870 test(
'should handle multiple rapid logo selections', async () => {
874 await
new Promise(resolve => setTimeout(resolve, 100));
876 const startTime = Date.now();
879 for (let i = 0; i < 10; i++) {
887 const endTime = Date.now();
888 const duration = endTime - startTime;
891 assert.ok(duration < 5000,
'Multiple selections should be reasonably fast');
898 test(
'should reuse file loader instances efficiently', async () => {
902 await
new Promise(resolve => setTimeout(resolve, 100));
905 const logo1 = await
randomLogo.getRandomLogoFromFolder();
906 const logo2 = await
randomLogo.getRandomLogoFromFolder();
908 assert.ok(logo1.fileName.endsWith(
'.txt'));
909 assert.ok(logo2.fileName.endsWith(
'.txt'));
912 assert.ok(Array.isArray(logo1.logoContent));
913 assert.ok(Array.isArray(logo2.logoContent));
920 test(
'should handle large numbers of logo files', async () => {
922 const manyLogosDir =
path.join(tempDir,
'many-logos');
923 await
fs.mkdir(manyLogosDir, { recursive:
true });
926 for (let i = 0; i < 50; i++) {
927 const content = `Logo ${i}\n${
'*'.repeat(i % 10 + 1)}\nEnd`;
928 await
fs.writeFile(
path.join(manyLogosDir, `
logo-${i}.txt`), content,
'utf8');
931 const randomLogo =
new RandomLogo(manyLogosDir);
934 await
new Promise(resolve => setTimeout(resolve, 200));
946 test(
'should handle directory updates without memory leaks', async () => {
950 await
new Promise(resolve => setTimeout(resolve, 100));
968 suite(
'File System Integration', () => {
973 test(
'should handle symbolic links appropriately', async () => {
976 const symlinkDir =
path.join(tempDir,
'symlink-test');
977 await
fs.mkdir(symlinkDir, { recursive:
true });
980 const targetFile =
path.join(logoDir,
'simple-logo.txt');
981 const symlinkFile =
path.join(symlinkDir,
'symlinked-logo.txt');
983 await
fs.symlink(targetFile, symlinkFile);
985 const randomLogo =
new RandomLogo(symlinkDir);
988 await
new Promise(resolve => setTimeout(resolve, 50));
995 console.log(
'Skipping symlink test - not supported on this system');
1003 test(
'should respect file system permissions', async () => {
1007 const result = await
randomLogo.updateRootDir(
'/etc');
1015 assert.ok(error instanceof Error);
1019 assert.ok(
true,
'Permission handling works correctly');
Mock implementation of VS Code webview panel for testing.
constructor(public viewType:string, public title:string, public showOptions:vscode.ViewColumn, public options:vscode.WebviewPanelOptions &vscode.WebviewOptions)
Constructs a new mock webview panel with specified configuration.
Mock implementation of VS Code webview for testing.
postMessage(message:any)
Posts a message to all registered handlers.
onDidReceiveMessage(handler:(message:any)=> void)
Registers a message handler for webview communication.
get html()
Gets the current HTML content of the webview.
export const randomLogo
Whether to use random logo selection instead of default logo.
Structure representing a loaded ASCII art logo with metadata.
export const Record< string,(...args:any[])=> string
import *as vscode from vscode
import *as path from path
import *as assert from assert
import *as vscode from vscode