import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { ensureExtensionCopy, ensureExtensionCopyAsync, shouldCopyYomitanExtension, } from './yomitan-extension-copy'; import { withSuppressedYomitanExtensionWarnings } from './yomitan-extension-loader'; function makeTempDir(prefix: string): string { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } function writeFile(filePath: string, content: string): void { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, content, 'utf-8'); } test('suppresses Yomitan contextMenus extension load warnings only while loading', async () => { const emitted: string[] = []; const warningProcess = { emitWarning: (warning: string | Error, options?: { type?: string }) => { const message = warning instanceof Error ? warning.message : warning; emitted.push(`${options?.type ?? ''}:${message}`); }, } as Pick; await withSuppressedYomitanExtensionWarnings(async () => { warningProcess.emitWarning( "Warnings loading extension:\nPermission 'contextMenus' is unknown.", { type: 'ExtensionLoadWarning', }, ); warningProcess.emitWarning('Other extension warning', { type: 'ExtensionLoadWarning' }); return null; }, warningProcess); warningProcess.emitWarning("Permission 'contextMenus' is unknown.", { type: 'ExtensionLoadWarning', }); assert.deepEqual(emitted, [ 'ExtensionLoadWarning:Other extension warning', "ExtensionLoadWarning:Permission 'contextMenus' is unknown.", ]); }); test('suppressed Yomitan warning wrapper is re-entrant safe', async () => { const emitted: string[] = []; const warningProcess = { emitWarning: (warning: string | Error, options?: { type?: string }) => { const message = warning instanceof Error ? warning.message : warning; emitted.push(`${options?.type ?? ''}:${message}`); }, } as Pick; const originalEmitWarning = warningProcess.emitWarning; await withSuppressedYomitanExtensionWarnings(async () => { await withSuppressedYomitanExtensionWarnings(async () => { warningProcess.emitWarning("Permission 'contextMenus' is unknown.", { type: 'ExtensionLoadWarning', }); warningProcess.emitWarning('Nested warning', { type: 'ExtensionLoadWarning' }); }, warningProcess); warningProcess.emitWarning("Permission 'contextMenus' is unknown.", { type: 'ExtensionLoadWarning', }); warningProcess.emitWarning('Outer warning', { type: 'ExtensionLoadWarning' }); }, warningProcess); assert.equal(warningProcess.emitWarning, originalEmitWarning); assert.deepEqual(emitted, [ 'ExtensionLoadWarning:Nested warning', 'ExtensionLoadWarning:Outer warning', ]); }); test('shouldCopyYomitanExtension detects popup runtime script drift', () => { const tempRoot = makeTempDir('subminer-yomitan-copy-'); const sourceDir = path.join(tempRoot, 'source'); const targetDir = path.join(tempRoot, 'target'); writeFile(path.join(sourceDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); writeFile(path.join(targetDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); writeFile(path.join(sourceDir, 'js', 'app', 'popup.js'), 'same-popup-script'); writeFile(path.join(targetDir, 'js', 'app', 'popup.js'), 'same-popup-script'); writeFile(path.join(sourceDir, 'js', 'display', 'popup-main.js'), 'source-popup-main'); writeFile(path.join(targetDir, 'js', 'display', 'popup-main.js'), 'target-popup-main'); assert.equal(shouldCopyYomitanExtension(sourceDir, targetDir), true); }); test('shouldCopyYomitanExtension skips copy when extension contents match', () => { const tempRoot = makeTempDir('subminer-yomitan-copy-'); const sourceDir = path.join(tempRoot, 'source'); const targetDir = path.join(tempRoot, 'target'); writeFile(path.join(sourceDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); writeFile(path.join(targetDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); writeFile(path.join(sourceDir, 'js', 'app', 'popup.js'), 'same-popup-script'); writeFile(path.join(targetDir, 'js', 'app', 'popup.js'), 'same-popup-script'); writeFile(path.join(sourceDir, 'js', 'display', 'popup-main.js'), 'same-popup-main'); writeFile(path.join(targetDir, 'js', 'display', 'popup-main.js'), 'same-popup-main'); writeFile(path.join(sourceDir, 'js', 'display', 'display.js'), 'same-display'); writeFile(path.join(targetDir, 'js', 'display', 'display.js'), 'same-display'); writeFile(path.join(sourceDir, 'js', 'display', 'display-audio.js'), 'same-display-audio'); writeFile(path.join(targetDir, 'js', 'display', 'display-audio.js'), 'same-display-audio'); assert.equal(shouldCopyYomitanExtension(sourceDir, targetDir), false); }); test('ensureExtensionCopy refreshes copied extension when display files change', () => { const sourceRoot = makeTempDir('subminer-yomitan-src-'); const userDataRoot = makeTempDir('subminer-yomitan-user-'); const sourceDir = path.join(sourceRoot, 'yomitan'); const targetDir = path.join(userDataRoot, 'extensions', 'yomitan'); fs.mkdirSync(path.join(sourceDir, 'js', 'display'), { recursive: true }); fs.mkdirSync(path.join(targetDir, 'js', 'display'), { recursive: true }); fs.writeFileSync(path.join(sourceDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); fs.writeFileSync(path.join(targetDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); fs.writeFileSync( path.join(sourceDir, 'js', 'display', 'structured-content-generator.js'), 'new display code', ); fs.writeFileSync( path.join(targetDir, 'js', 'display', 'structured-content-generator.js'), 'old display code', ); const result = ensureExtensionCopy(sourceDir, userDataRoot, { platform: 'linux' }); assert.equal(result.targetDir, targetDir); assert.equal(result.copied, true); assert.equal( fs.readFileSync( path.join(targetDir, 'js', 'display', 'structured-content-generator.js'), 'utf8', ), 'new display code', ); }); test('ensureExtensionCopyAsync refreshes copied extension without completing synchronously', async () => { const sourceRoot = makeTempDir('subminer-yomitan-src-'); const userDataRoot = makeTempDir('subminer-yomitan-user-'); const sourceDir = path.join(sourceRoot, 'yomitan'); const targetDir = path.join(userDataRoot, 'extensions', 'yomitan'); fs.mkdirSync(path.join(sourceDir, 'js', 'display'), { recursive: true }); fs.mkdirSync(path.join(targetDir, 'js', 'display'), { recursive: true }); fs.writeFileSync(path.join(sourceDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); fs.writeFileSync(path.join(targetDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); fs.writeFileSync( path.join(sourceDir, 'js', 'display', 'structured-content-generator.js'), 'new display code', ); fs.writeFileSync( path.join(targetDir, 'js', 'display', 'structured-content-generator.js'), 'old display code', ); let completed = false; const resultPromise = ensureExtensionCopyAsync(sourceDir, userDataRoot, { platform: 'linux', }).then((result) => { completed = true; return result; }); assert.equal(completed, false); const result = await resultPromise; assert.equal(result.targetDir, targetDir); assert.equal(result.copied, true); assert.equal( fs.readFileSync( path.join(targetDir, 'js', 'display', 'structured-content-generator.js'), 'utf8', ), 'new display code', ); }); test('ensureExtensionCopyAsync shares an in-flight refresh for the same copied extension', async () => { const sourceRoot = makeTempDir('subminer-yomitan-src-'); const userDataRoot = makeTempDir('subminer-yomitan-user-'); const sourceDir = path.join(sourceRoot, 'yomitan'); const targetDir = path.join(userDataRoot, 'extensions', 'yomitan'); fs.mkdirSync(path.join(sourceDir, 'js', 'pages', 'settings'), { recursive: true }); fs.mkdirSync(path.join(targetDir, 'js', 'pages', 'settings'), { recursive: true }); fs.writeFileSync(path.join(sourceDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); fs.writeFileSync(path.join(targetDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); fs.writeFileSync( path.join(sourceDir, 'js', 'pages', 'settings', 'settings-main.js'), 'new settings code', ); fs.writeFileSync( path.join(targetDir, 'js', 'pages', 'settings', 'settings-main.js'), 'old settings code', ); const originalCp = fs.promises.cp; let cpCalls = 0; let firstCopyStarted = false; let releaseFirstCopy: () => void = () => {}; const firstCopyStartedPromise = new Promise((resolve) => { const fsPromises = fs.promises as typeof fs.promises & { cp: typeof fs.promises.cp; }; fsPromises.cp = async (...args: Parameters) => { cpCalls++; if (!firstCopyStarted) { firstCopyStarted = true; resolve(); await new Promise((release) => { releaseFirstCopy = release; }); } return await originalCp(...args); }; }); try { const first = ensureExtensionCopyAsync(sourceDir, userDataRoot, { platform: 'linux' }); await firstCopyStartedPromise; const second = ensureExtensionCopyAsync(sourceDir, userDataRoot, { platform: 'linux' }); releaseFirstCopy(); const results = await Promise.all([first, second]); assert.equal(cpCalls, 1); assert.equal(results[0].targetDir, targetDir); assert.equal(results[1].targetDir, targetDir); assert.equal(results[0].copied, true); assert.equal(results[1].copied, true); assert.equal( fs.readFileSync(path.join(targetDir, 'js', 'pages', 'settings', 'settings-main.js'), 'utf8'), 'new settings code', ); } finally { const fsPromises = fs.promises as typeof fs.promises & { cp: typeof fs.promises.cp; }; fsPromises.cp = originalCp; } });