mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-30 07:21:32 -07:00
fix(overlay): Linux X11/XWayland stacking, stale pause state, multi-copy selector (#101)
This commit is contained in:
@@ -57,6 +57,7 @@ class FakeElement {
|
||||
dataset: Record<string, string> = {};
|
||||
style = new FakeStyleDeclaration();
|
||||
className = '';
|
||||
replaceChildrenCalls = 0;
|
||||
private ownTextContent = '';
|
||||
|
||||
constructor(public tagName: string) {}
|
||||
@@ -97,6 +98,7 @@ class FakeElement {
|
||||
}
|
||||
|
||||
replaceChildren(): void {
|
||||
this.replaceChildrenCalls += 1;
|
||||
this.childNodes = [];
|
||||
this.ownTextContent = '';
|
||||
}
|
||||
@@ -347,6 +349,130 @@ test('renderSubtitle skips character image when name-match rendering is disabled
|
||||
}
|
||||
});
|
||||
|
||||
test('renderSubtitle skips identical primary subtitle DOM replacement', () => {
|
||||
const restoreDocument = installFakeDocument();
|
||||
try {
|
||||
const subtitleRoot = new FakeElement('div');
|
||||
const ctx = {
|
||||
state: createRendererState(),
|
||||
dom: {
|
||||
subtitleRoot,
|
||||
subtitleContainer: new FakeElement('div'),
|
||||
secondarySubRoot: new FakeElement('div'),
|
||||
secondarySubContainer: new FakeElement('div'),
|
||||
},
|
||||
} as never;
|
||||
|
||||
const renderer = createSubtitleRenderer(ctx);
|
||||
renderer.renderSubtitle({ text: '字幕', tokens: null });
|
||||
renderer.renderSubtitle({ text: '字幕', tokens: null });
|
||||
renderer.renderSubtitle({ text: '字幕2', tokens: null });
|
||||
|
||||
assert.equal(subtitleRoot.replaceChildrenCalls, 2);
|
||||
assert.equal(subtitleRoot.textContent, '字幕2');
|
||||
} finally {
|
||||
restoreDocument();
|
||||
}
|
||||
});
|
||||
|
||||
test('renderSubtitle keeps tokenized subtitle when stale plain payload repeats same text', () => {
|
||||
const restoreDocument = installFakeDocument();
|
||||
try {
|
||||
const subtitleRoot = new FakeElement('div');
|
||||
const ctx = {
|
||||
state: createRendererState(),
|
||||
dom: {
|
||||
subtitleRoot,
|
||||
subtitleContainer: new FakeElement('div'),
|
||||
secondarySubRoot: new FakeElement('div'),
|
||||
secondarySubContainer: new FakeElement('div'),
|
||||
},
|
||||
} as never;
|
||||
|
||||
const renderer = createSubtitleRenderer(ctx);
|
||||
renderer.renderSubtitle({
|
||||
text: 'アクア',
|
||||
tokens: [createToken({ surface: 'アクア', headword: 'アクア', reading: 'あくあ' })],
|
||||
});
|
||||
renderer.renderSubtitle({ text: 'アクア', tokens: null });
|
||||
|
||||
assert.equal(subtitleRoot.replaceChildrenCalls, 1);
|
||||
assert.equal(collectWordNodes(subtitleRoot).length, 1);
|
||||
assert.equal(subtitleRoot.textContent, 'アクア');
|
||||
} finally {
|
||||
restoreDocument();
|
||||
}
|
||||
});
|
||||
|
||||
test('renderSubtitle accepts repeated plain payload after style invalidates tokenized render', () => {
|
||||
const restoreDocument = installFakeDocument();
|
||||
try {
|
||||
const subtitleRoot = new FakeElement('div');
|
||||
const ctx = {
|
||||
state: createRendererState(),
|
||||
dom: {
|
||||
subtitleRoot,
|
||||
subtitleContainer: new FakeElement('div'),
|
||||
secondarySubRoot: new FakeElement('div'),
|
||||
secondarySubContainer: new FakeElement('div'),
|
||||
},
|
||||
} as never;
|
||||
|
||||
const renderer = createSubtitleRenderer(ctx);
|
||||
renderer.renderSubtitle({
|
||||
text: 'アクア',
|
||||
tokens: [createToken({ surface: 'アクア', headword: 'アクア', reading: 'あくあ' })],
|
||||
});
|
||||
renderer.applySubtitleStyle({ fontColor: '#fff' } as never);
|
||||
renderer.renderSubtitle({ text: 'アクア', tokens: null });
|
||||
|
||||
assert.equal(subtitleRoot.replaceChildrenCalls, 2);
|
||||
assert.equal(collectWordNodes(subtitleRoot).length, 0);
|
||||
assert.equal(subtitleRoot.textContent, 'アクア');
|
||||
} finally {
|
||||
restoreDocument();
|
||||
}
|
||||
});
|
||||
|
||||
test('renderSubtitle re-renders identical text after style changes affect token output', () => {
|
||||
const restoreDocument = installFakeDocument();
|
||||
try {
|
||||
const subtitleRoot = new FakeElement('div');
|
||||
const ctx = {
|
||||
state: {
|
||||
...createRendererState(),
|
||||
nameMatchEnabled: false,
|
||||
},
|
||||
dom: {
|
||||
subtitleRoot,
|
||||
subtitleContainer: new FakeElement('div'),
|
||||
secondarySubRoot: new FakeElement('div'),
|
||||
secondarySubContainer: new FakeElement('div'),
|
||||
},
|
||||
} as never;
|
||||
const subtitle = {
|
||||
text: 'アクア',
|
||||
tokens: [
|
||||
{
|
||||
...createToken({ surface: 'アクア', headword: 'アクア', reading: 'あくあ' }),
|
||||
isNameMatch: true,
|
||||
} as MergedToken,
|
||||
],
|
||||
};
|
||||
|
||||
const renderer = createSubtitleRenderer(ctx);
|
||||
renderer.renderSubtitle(subtitle);
|
||||
renderer.applySubtitleStyle({ nameMatchEnabled: true } as never);
|
||||
renderer.renderSubtitle(subtitle);
|
||||
|
||||
const [word] = collectWordNodes(subtitleRoot);
|
||||
assert.equal(subtitleRoot.replaceChildrenCalls, 2);
|
||||
assert.ok(word?.className.includes('word-name-match'));
|
||||
} finally {
|
||||
restoreDocument();
|
||||
}
|
||||
});
|
||||
|
||||
test('renderer content security policy allows data URL character images', () => {
|
||||
const htmlPath = path.join(process.cwd(), 'src', 'renderer', 'index.html');
|
||||
const htmlText = fs.readFileSync(htmlPath, 'utf-8');
|
||||
@@ -1231,6 +1357,13 @@ test('subtitle annotation CSS underlines JLPT tokens without changing token colo
|
||||
assert.match(secondaryHoverWindowsBlock, /top:\s*40px;/);
|
||||
assert.match(secondaryHoverWindowsBlock, /padding-top:\s*0;/);
|
||||
|
||||
const sidebarSettingsModalBlock = extractClassBlock(
|
||||
cssText,
|
||||
'body.settings-modal-open .subtitle-sidebar-modal',
|
||||
);
|
||||
assert.match(sidebarSettingsModalBlock, /display:\s*none !important;/);
|
||||
assert.match(sidebarSettingsModalBlock, /pointer-events:\s*none !important;/);
|
||||
|
||||
const subtitleSidebarListBlock = extractClassBlock(cssText, '.subtitle-sidebar-list');
|
||||
assert.doesNotMatch(subtitleSidebarListBlock, /scroll-behavior:\s*smooth;/);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user