mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-27 11:35:23 -07:00
test: cut low-value tests, rewrite the rest around behavior, parallelize lanes (#285)
This commit is contained in:
1 parent
4b6842d034
commit
b48083ddd8
368 files changed
+18045
-60331
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -1,109 +1,105 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import type { ResolvedControllerConfig } from '../../types';
|
||||
import { createControllerConfigForm } from './controller-config-form.js';
|
||||
|
||||
function createClassList(initialTokens: string[] = []) {
|
||||
const tokens = new Set(initialTokens);
|
||||
return {
|
||||
add: (...entries: string[]) => {
|
||||
for (const entry of entries) tokens.add(entry);
|
||||
type ActionId = keyof ResolvedControllerConfig['bindings'];
|
||||
|
||||
const BINDINGS: ResolvedControllerConfig['bindings'] = {
|
||||
toggleLookup: { kind: 'button', buttonIndex: 0 },
|
||||
closeLookup: { kind: 'button', buttonIndex: 1 },
|
||||
toggleKeyboardOnlyMode: { kind: 'button', buttonIndex: 3 },
|
||||
mineCard: { kind: 'button', buttonIndex: 2 },
|
||||
quitMpv: { kind: 'button', buttonIndex: 6 },
|
||||
previousAudio: { kind: 'none' },
|
||||
nextAudio: { kind: 'button', buttonIndex: 5 },
|
||||
playCurrentAudio: { kind: 'button', buttonIndex: 4 },
|
||||
toggleMpvPause: { kind: 'button', buttonIndex: 9 },
|
||||
leftStickHorizontal: { kind: 'axis', axisIndex: 0, dpadFallback: 'horizontal' },
|
||||
leftStickVertical: { kind: 'axis', axisIndex: 1, dpadFallback: 'vertical' },
|
||||
rightStickHorizontal: { kind: 'axis', axisIndex: 3, dpadFallback: 'none' },
|
||||
rightStickVertical: { kind: 'axis', axisIndex: 4, dpadFallback: 'none' },
|
||||
};
|
||||
|
||||
/** Minimal DOM element: enough for the form's createElement/appendChild/click usage. */
|
||||
class FakeElement {
|
||||
className = '';
|
||||
textContent = '';
|
||||
title = '';
|
||||
type = '';
|
||||
children: FakeElement[] = [];
|
||||
private readonly listeners = new Map<string, Array<(event: Event) => void>>();
|
||||
private readonly attributes = new Map<string, string>();
|
||||
|
||||
readonly classList = {
|
||||
add: (...tokens: string[]) => {
|
||||
this.className = [...new Set([...this.classes(), ...tokens])].join(' ');
|
||||
},
|
||||
remove: (...entries: string[]) => {
|
||||
for (const entry of entries) tokens.delete(entry);
|
||||
},
|
||||
toggle: (entry: string, force?: boolean) => {
|
||||
if (force === undefined) {
|
||||
if (tokens.has(entry)) tokens.delete(entry);
|
||||
else tokens.add(entry);
|
||||
return tokens.has(entry);
|
||||
}
|
||||
if (force) tokens.add(entry);
|
||||
else tokens.delete(entry);
|
||||
return force;
|
||||
},
|
||||
contains: (entry: string) => tokens.has(entry),
|
||||
contains: (token: string) => this.classes().includes(token),
|
||||
};
|
||||
|
||||
set innerHTML(value: string) {
|
||||
if (value === '') this.children = [];
|
||||
}
|
||||
|
||||
appendChild(child: FakeElement): FakeElement {
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
addEventListener(type: string, listener: (event: Event) => void): void {
|
||||
this.listeners.set(type, [...(this.listeners.get(type) ?? []), listener]);
|
||||
}
|
||||
|
||||
click(): void {
|
||||
const event = { stopPropagation: () => {}, preventDefault: () => {} } as unknown as Event;
|
||||
for (const listener of this.listeners.get('click') ?? []) listener(event);
|
||||
}
|
||||
|
||||
setAttribute(name: string, value: string): void {
|
||||
this.attributes.set(name, value);
|
||||
}
|
||||
|
||||
getAttribute(name: string): string | null {
|
||||
return this.attributes.get(name) ?? null;
|
||||
}
|
||||
|
||||
private classes(): string[] {
|
||||
return this.className.split(' ').filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
function createFakeElement() {
|
||||
const attributes = new Map<string, string>();
|
||||
const el = {
|
||||
className: '',
|
||||
textContent: '',
|
||||
_innerHTML: '',
|
||||
value: '',
|
||||
disabled: false,
|
||||
selected: false,
|
||||
type: '',
|
||||
children: [] as any[],
|
||||
listeners: new Map<string, Array<(e?: any) => void>>(),
|
||||
classList: createClassList(),
|
||||
appendChild(child: any) {
|
||||
this.children.push(child);
|
||||
return child;
|
||||
},
|
||||
addEventListener(type: string, listener: (e?: any) => void) {
|
||||
const existing = this.listeners.get(type) ?? [];
|
||||
existing.push(listener);
|
||||
this.listeners.set(type, existing);
|
||||
},
|
||||
dispatch(type: string) {
|
||||
const fakeEvent = { stopPropagation: () => {}, preventDefault: () => {} };
|
||||
for (const listener of this.listeners.get(type) ?? []) {
|
||||
listener(fakeEvent);
|
||||
}
|
||||
},
|
||||
setAttribute(name: string, value: string) {
|
||||
attributes.set(name, value);
|
||||
},
|
||||
getAttribute(name: string) {
|
||||
return attributes.get(name) ?? null;
|
||||
},
|
||||
};
|
||||
Object.defineProperty(el, 'innerHTML', {
|
||||
get() {
|
||||
return el._innerHTML;
|
||||
},
|
||||
set(v: string) {
|
||||
el._innerHTML = v;
|
||||
if (v === '') el.children.length = 0;
|
||||
},
|
||||
});
|
||||
return el;
|
||||
function findAll(root: FakeElement, className: string): FakeElement[] {
|
||||
return root.children.flatMap((child) => [
|
||||
...(child.classList.contains(className) ? [child] : []),
|
||||
...findAll(child, className),
|
||||
]);
|
||||
}
|
||||
|
||||
test('controller config form renders rows and dispatches learn clear reset callbacks', () => {
|
||||
const previousDocumentDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
function findOne(root: FakeElement, className: string, text?: string): FakeElement {
|
||||
const match = findAll(root, className).find(
|
||||
(el) => text === undefined || el.textContent === text,
|
||||
);
|
||||
assert.ok(match, `missing .${className}${text === undefined ? '' : ` "${text}"`}`);
|
||||
return match;
|
||||
}
|
||||
|
||||
/** Renders the form into a fake container with `document` stubbed for the duration. */
|
||||
function renderForm(learningActionId: ActionId | null) {
|
||||
const previousDocument = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement: () => createFakeElement(),
|
||||
},
|
||||
value: { createElement: () => new FakeElement() },
|
||||
});
|
||||
|
||||
try {
|
||||
const calls: string[] = [];
|
||||
const container = createFakeElement();
|
||||
const form = createControllerConfigForm({
|
||||
container: container as never,
|
||||
getBindings: () =>
|
||||
({
|
||||
toggleLookup: { kind: 'button', buttonIndex: 0 },
|
||||
closeLookup: { kind: 'button', buttonIndex: 1 },
|
||||
toggleKeyboardOnlyMode: { kind: 'button', buttonIndex: 3 },
|
||||
mineCard: { kind: 'button', buttonIndex: 2 },
|
||||
quitMpv: { kind: 'button', buttonIndex: 6 },
|
||||
previousAudio: { kind: 'none' },
|
||||
nextAudio: { kind: 'button', buttonIndex: 5 },
|
||||
playCurrentAudio: { kind: 'button', buttonIndex: 4 },
|
||||
toggleMpvPause: { kind: 'button', buttonIndex: 9 },
|
||||
leftStickHorizontal: { kind: 'axis', axisIndex: 0, dpadFallback: 'horizontal' },
|
||||
leftStickVertical: { kind: 'axis', axisIndex: 1, dpadFallback: 'vertical' },
|
||||
rightStickHorizontal: { kind: 'axis', axisIndex: 3, dpadFallback: 'none' },
|
||||
rightStickVertical: { kind: 'axis', axisIndex: 4, dpadFallback: 'none' },
|
||||
}) as never,
|
||||
getLearningActionId: () => 'toggleLookup',
|
||||
const container = new FakeElement();
|
||||
createControllerConfigForm({
|
||||
container: container as unknown as HTMLElement,
|
||||
getBindings: () => BINDINGS,
|
||||
getLearningActionId: () => learningActionId,
|
||||
getDpadLearningActionId: () => null,
|
||||
onLearn: (actionId, bindingType) => calls.push(`learn:${actionId}:${bindingType}`),
|
||||
onClear: (actionId) => calls.push(`clear:${actionId}`),
|
||||
@@ -111,102 +107,123 @@ test('controller config form renders rows and dispatches learn clear reset callb
|
||||
onDpadLearn: (actionId) => calls.push(`dpadLearn:${actionId}`),
|
||||
onDpadClear: (actionId) => calls.push(`dpadClear:${actionId}`),
|
||||
onDpadReset: (actionId) => calls.push(`dpadReset:${actionId}`),
|
||||
});
|
||||
}).render();
|
||||
|
||||
form.render();
|
||||
|
||||
// In the new compact list layout, children are:
|
||||
// [0] group header, [1] first binding row (auto-expanded because learning), [2] edit panel, [3] next row, ...
|
||||
const firstRow = container.children[1];
|
||||
assert.equal(firstRow.classList.contains('expanded'), true);
|
||||
|
||||
// After expanding, the edit panel is inserted after the row:
|
||||
// [0] group header, [1] row, [2] edit panel, [3] next row, ...
|
||||
const editPanel = container.children[2];
|
||||
// editPanel > inner > actions > learnButton
|
||||
const inner = editPanel.children[0];
|
||||
const actions = inner.children[1];
|
||||
const learnButton = actions.children[0];
|
||||
learnButton.dispatch('click');
|
||||
actions.children[1].dispatch('click');
|
||||
actions.children[2].dispatch('click');
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'learn:toggleLookup:discrete',
|
||||
'clear:toggleLookup',
|
||||
'reset:toggleLookup',
|
||||
]);
|
||||
const row = (label: string) => {
|
||||
const match = findAll(container, 'controller-config-row').find(
|
||||
(el) => findOne(el, 'controller-config-label').textContent === label,
|
||||
);
|
||||
assert.ok(match, `missing row "${label}"`);
|
||||
return match;
|
||||
};
|
||||
return { calls, container, row };
|
||||
} finally {
|
||||
if (previousDocumentDescriptor) {
|
||||
Object.defineProperty(globalThis, 'document', previousDocumentDescriptor);
|
||||
if (previousDocument) {
|
||||
Object.defineProperty(globalThis, 'document', previousDocument);
|
||||
} else {
|
||||
Reflect.deleteProperty(globalThis, 'document');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('controller config form renders grouped rows with friendly binding labels', () => {
|
||||
const { container } = renderForm(null);
|
||||
|
||||
assert.deepEqual(
|
||||
findAll(container, 'controller-config-group').map((el) => el.textContent),
|
||||
['Lookup', 'Playback', 'Popup Audio', 'Navigation'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
findAll(container, 'controller-config-row').map((row) => [
|
||||
findOne(row, 'controller-config-label').textContent,
|
||||
findOne(row, 'controller-config-badge').textContent,
|
||||
]),
|
||||
[
|
||||
['Toggle Lookup', 'A / Cross'],
|
||||
['Close Lookup', 'B / Circle'],
|
||||
['Mine Card', 'X / Square'],
|
||||
['Toggle Keyboard-Only Mode', 'Y / Triangle'],
|
||||
['Toggle MPV Pause', 'R3 / RS'],
|
||||
['Quit MPV', 'Back / Select'],
|
||||
['Previous Audio', 'None'],
|
||||
['Next Audio', 'RB / R1'],
|
||||
['Play Current Audio', 'LB / L1'],
|
||||
['Token Move (Stick)', 'Left Stick X'],
|
||||
['Token Move (D-pad)', 'D-pad ↔'],
|
||||
['Popup Scroll (Stick)', 'Left Stick Y'],
|
||||
['Popup Scroll (D-pad)', 'D-pad ↕'],
|
||||
['Alt Horizontal (Stick)', 'Right Stick X'],
|
||||
['Alt Horizontal (D-pad)', 'None'],
|
||||
['Popup Jump (Stick)', 'Right Stick Y'],
|
||||
['Popup Jump (D-pad)', 'None'],
|
||||
],
|
||||
);
|
||||
assert.deepEqual(
|
||||
findAll(container, 'disabled').map((badge) => badge.textContent),
|
||||
['None', 'None', 'None'],
|
||||
);
|
||||
assert.deepEqual(findAll(container, 'expanded'), []);
|
||||
assert.deepEqual(findAll(container, 'controller-config-edit-panel'), []);
|
||||
});
|
||||
|
||||
test('controller config form starts learn from badge or edit and resets from row button', () => {
|
||||
const previousDocumentDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement: () => createFakeElement(),
|
||||
},
|
||||
test('controller config form expands the learning row with a listening edit panel', () => {
|
||||
const { container, row } = renderForm('toggleLookup');
|
||||
|
||||
assert.deepEqual(
|
||||
findAll(container, 'expanded').map((el) => findOne(el, 'controller-config-label').textContent),
|
||||
['Toggle Lookup'],
|
||||
);
|
||||
// The edit panel is inserted directly after the expanded row.
|
||||
const rowIndex = container.children.indexOf(row('Toggle Lookup'));
|
||||
const panel = container.children[rowIndex + 1];
|
||||
assert.ok(panel);
|
||||
assert.equal(panel.classList.contains('controller-config-edit-panel'), true);
|
||||
|
||||
const hint = findOne(panel, 'controller-config-edit-hint');
|
||||
assert.equal(hint.textContent, 'Press a button, trigger, or move a stick…');
|
||||
assert.equal(hint.classList.contains('learning'), true);
|
||||
assert.equal(findOne(panel, 'btn-learn').textContent, 'Listening…');
|
||||
});
|
||||
|
||||
type Locate = (row: FakeElement, panel: FakeElement) => FakeElement;
|
||||
|
||||
for (const c of [
|
||||
{
|
||||
control: 'row badge',
|
||||
locate: (row) => findOne(row, 'controller-config-badge'),
|
||||
expected: 'learn:toggleLookup:discrete',
|
||||
},
|
||||
{
|
||||
control: 'row reset icon',
|
||||
locate: (row) => findOne(row, 'controller-config-reset-icon'),
|
||||
expected: 'reset:toggleLookup',
|
||||
},
|
||||
{
|
||||
control: 'row edit icon',
|
||||
locate: (row) => findOne(row, 'controller-config-edit-icon'),
|
||||
expected: 'learn:toggleLookup:discrete',
|
||||
},
|
||||
{
|
||||
control: 'panel Learn',
|
||||
locate: (_row, panel) => findOne(panel, 'btn-learn'),
|
||||
expected: 'learn:toggleLookup:discrete',
|
||||
},
|
||||
{
|
||||
control: 'panel Clear',
|
||||
locate: (_row, panel) => findOne(panel, 'btn-secondary', 'Clear'),
|
||||
expected: 'clear:toggleLookup',
|
||||
},
|
||||
{
|
||||
control: 'panel Reset',
|
||||
locate: (_row, panel) => findOne(panel, 'btn-secondary', 'Reset'),
|
||||
expected: 'reset:toggleLookup',
|
||||
},
|
||||
] satisfies Array<{ control: string; locate: Locate; expected: string }>) {
|
||||
test(`controller config form ${c.control} dispatches ${c.expected}`, () => {
|
||||
const { calls, container, row } = renderForm('toggleLookup');
|
||||
|
||||
c.locate(row('Toggle Lookup'), findOne(container, 'controller-config-edit-panel')).click();
|
||||
|
||||
assert.deepEqual(calls, [c.expected]);
|
||||
});
|
||||
|
||||
try {
|
||||
const calls: string[] = [];
|
||||
const container = createFakeElement();
|
||||
const form = createControllerConfigForm({
|
||||
container: container as never,
|
||||
getBindings: () =>
|
||||
({
|
||||
toggleLookup: { kind: 'button', buttonIndex: 0 },
|
||||
closeLookup: { kind: 'button', buttonIndex: 1 },
|
||||
toggleKeyboardOnlyMode: { kind: 'button', buttonIndex: 3 },
|
||||
mineCard: { kind: 'button', buttonIndex: 2 },
|
||||
quitMpv: { kind: 'button', buttonIndex: 6 },
|
||||
previousAudio: { kind: 'none' },
|
||||
nextAudio: { kind: 'button', buttonIndex: 5 },
|
||||
playCurrentAudio: { kind: 'button', buttonIndex: 4 },
|
||||
toggleMpvPause: { kind: 'button', buttonIndex: 9 },
|
||||
leftStickHorizontal: { kind: 'axis', axisIndex: 0, dpadFallback: 'horizontal' },
|
||||
leftStickVertical: { kind: 'axis', axisIndex: 1, dpadFallback: 'vertical' },
|
||||
rightStickHorizontal: { kind: 'axis', axisIndex: 3, dpadFallback: 'none' },
|
||||
rightStickVertical: { kind: 'axis', axisIndex: 4, dpadFallback: 'none' },
|
||||
}) as never,
|
||||
getLearningActionId: () => null,
|
||||
getDpadLearningActionId: () => null,
|
||||
onLearn: (actionId, bindingType) => calls.push(`learn:${actionId}:${bindingType}`),
|
||||
onClear: (actionId) => calls.push(`clear:${actionId}`),
|
||||
onReset: (actionId) => calls.push(`reset:${actionId}`),
|
||||
onDpadLearn: (actionId) => calls.push(`dpadLearn:${actionId}`),
|
||||
onDpadClear: (actionId) => calls.push(`dpadClear:${actionId}`),
|
||||
onDpadReset: (actionId) => calls.push(`dpadReset:${actionId}`),
|
||||
});
|
||||
|
||||
form.render();
|
||||
|
||||
const firstRow = container.children[1];
|
||||
const right = firstRow.children[1];
|
||||
const badge = right.children[0];
|
||||
const resetButton = right.children[1];
|
||||
const editButton = right.children[2];
|
||||
|
||||
badge.dispatch('click');
|
||||
resetButton.dispatch('click');
|
||||
editButton.dispatch('click');
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'learn:toggleLookup:discrete',
|
||||
'reset:toggleLookup',
|
||||
'learn:toggleLookup:discrete',
|
||||
]);
|
||||
} finally {
|
||||
if (previousDocumentDescriptor) {
|
||||
Object.defineProperty(globalThis, 'document', previousDocumentDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(globalThis, 'document');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -301,31 +301,6 @@ function setupPlaylistBrowserModalTest(options?: {
|
||||
};
|
||||
}
|
||||
|
||||
test('playlist browser test cleanup must delete injected globals that were originally absent', () => {
|
||||
const previousWindowDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
||||
const previousDocumentDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
try {
|
||||
Reflect.deleteProperty(globalThis, 'window');
|
||||
Reflect.deleteProperty(globalThis, 'document');
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'window'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'document'), false);
|
||||
|
||||
const env = setupPlaylistBrowserModalTest();
|
||||
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'window'), true);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'document'), true);
|
||||
env.restore();
|
||||
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'window'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'document'), false);
|
||||
assert.equal(typeof globalThis.window, 'undefined');
|
||||
assert.equal(typeof globalThis.document, 'undefined');
|
||||
} finally {
|
||||
restoreGlobalDescriptor('window', previousWindowDescriptor);
|
||||
restoreGlobalDescriptor('document', previousDocumentDescriptor);
|
||||
}
|
||||
});
|
||||
|
||||
test('playlist browser modal opens with playlist-focused current item selection', async () => {
|
||||
const notifications: string[] = [];
|
||||
const env = setupPlaylistBrowserModalTest({
|
||||
@@ -354,37 +329,6 @@ test('playlist browser modal opens with playlist-focused current item selection'
|
||||
}
|
||||
});
|
||||
|
||||
test('playlist browser modal action buttons stop double-click propagation', async () => {
|
||||
const env = setupPlaylistBrowserModalTest();
|
||||
|
||||
try {
|
||||
const modal = env.createModal();
|
||||
|
||||
await modal.openPlaylistBrowserModal();
|
||||
|
||||
const row = env.dom.playlistBrowserDirectoryList.children[0] as
|
||||
| ReturnType<typeof createPlaylistRow>
|
||||
| undefined;
|
||||
const trailing = row?.children?.[1] as ReturnType<typeof createPlaylistRow> | undefined;
|
||||
const button = trailing?.children?.at(-1) as
|
||||
| { listeners?: Map<string, Array<(event?: unknown) => void>> }
|
||||
| undefined;
|
||||
const dblclickHandler = button?.listeners?.get('dblclick')?.[0];
|
||||
|
||||
assert.equal(typeof dblclickHandler, 'function');
|
||||
let stopped = false;
|
||||
dblclickHandler?.({
|
||||
stopPropagation: () => {
|
||||
stopped = true;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(stopped, true);
|
||||
} finally {
|
||||
env.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('playlist browser preserves prior selection across mutation snapshots', async () => {
|
||||
const env = setupPlaylistBrowserModalTest({
|
||||
electronApi: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import test from 'node:test';
|
||||
|
||||
import type { ElectronAPI, RuntimeOptionState } from '../../types';
|
||||
import { createRendererState } from '../state.js';
|
||||
import { createRuntimeOptionsModal } from './runtime-options.js';
|
||||
import { createRuntimeOptionsModal, scrollActiveRuntimeOptionIntoView } from './runtime-options.js';
|
||||
|
||||
function createClassList(initialTokens: string[] = []) {
|
||||
const tokens = new Set(initialTokens);
|
||||
@@ -219,3 +219,13 @@ test('openRuntimeOptionsModal keeps modal visible when loading fails', async ()
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('scrollActiveRuntimeOptionIntoView no-ops without active option', () => {
|
||||
const list = {
|
||||
querySelector: () => null,
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
scrollActiveRuntimeOptionIntoView(list);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,4 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import { createRendererState } from '../state.js';
|
||||
@@ -35,16 +33,6 @@ test('session help normalizes configured modifier aliases', () => {
|
||||
assert.equal(formatSessionHelpKeybinding('CommandOrControl+KeyS'), 'Cmd/Ctrl + S');
|
||||
});
|
||||
|
||||
test('session help imports browser-safe special command constants', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(process.cwd(), 'src', 'renderer', 'modals', 'session-help-sections.ts'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
assert.match(source, /from ['"]\.\.\/\.\.\/config\/definitions\/shared['"]/);
|
||||
assert.doesNotMatch(source, /from ['"]\.\.\/\.\.\/config\/definitions['"]/);
|
||||
});
|
||||
|
||||
test('session help builds rows from canonical session bindings and fixed overlay affordances', () => {
|
||||
const sections = buildSessionHelpSections({
|
||||
sessionBindings: [
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,4 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import type { TsukihimeSubtitleFile, ElectronAPI } from '../../types';
|
||||
@@ -249,26 +247,18 @@ function createModalHarness(
|
||||
};
|
||||
}
|
||||
|
||||
test('TsukiHime language tabs expose tab semantics in the renderer markup', () => {
|
||||
const html = fs.readFileSync(path.join(process.cwd(), 'src', 'renderer', 'index.html'), 'utf8');
|
||||
const tabs = html.match(/<div class="tsukihime-tabs"[\s\S]*?<\/div>/)?.[0];
|
||||
|
||||
assert.ok(tabs);
|
||||
assert.match(tabs, /<div class="tsukihime-tabs" role="tablist">/);
|
||||
assert.match(tabs, /id="tsukihimeTabSecondary"[\s\S]*?role="tab"[\s\S]*?aria-selected="true"/);
|
||||
assert.match(tabs, /id="tsukihimeTabPrimary"[\s\S]*?role="tab"[\s\S]*?aria-selected="false"/);
|
||||
});
|
||||
|
||||
test('switching TsukiHime language tabs synchronizes aria-selected', () => {
|
||||
const harness = createModalHarness([ENGLISH_TRACK, JAPANESE_TRACK]);
|
||||
try {
|
||||
pressKey(harness, 'ArrowRight');
|
||||
|
||||
assert.equal(harness.state.tsukihimeActiveTab, 'primary');
|
||||
assert.equal(harness.secondaryTab.attributes.get('aria-selected'), 'false');
|
||||
assert.equal(harness.primaryTab.attributes.get('aria-selected'), 'true');
|
||||
|
||||
pressKey(harness, 'ArrowLeft');
|
||||
|
||||
assert.equal(harness.state.tsukihimeActiveTab, 'secondary');
|
||||
assert.equal(harness.secondaryTab.attributes.get('aria-selected'), 'true');
|
||||
assert.equal(harness.primaryTab.attributes.get('aria-selected'), 'false');
|
||||
} finally {
|
||||
@@ -532,18 +522,6 @@ test('a slow release response does not overwrite the newly selected release', as
|
||||
}
|
||||
});
|
||||
|
||||
test('ArrowLeft switches back to the secondary-language tab', () => {
|
||||
const harness = createModalHarness([ENGLISH_TRACK, JAPANESE_TRACK]);
|
||||
try {
|
||||
pressKey(harness, 'ArrowRight');
|
||||
assert.equal(harness.state.tsukihimeActiveTab, 'primary');
|
||||
pressKey(harness, 'ArrowLeft');
|
||||
assert.equal(harness.state.tsukihimeActiveTab, 'secondary');
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('searching reports TsukiHime as the backend and lists sublangs per release', async () => {
|
||||
const harness = createModalHarness([], {
|
||||
searchEntries: async () => ({
|
||||
|
||||
@@ -92,17 +92,6 @@ function restoreGlobalProp<K extends keyof typeof globalThis>(
|
||||
Reflect.deleteProperty(globalThis, key);
|
||||
}
|
||||
|
||||
function restoreGlobalDescriptor<K extends keyof typeof globalThis>(
|
||||
key: K,
|
||||
descriptor: PropertyDescriptor | undefined,
|
||||
) {
|
||||
if (descriptor) {
|
||||
Object.defineProperty(globalThis, key, descriptor);
|
||||
return;
|
||||
}
|
||||
Reflect.deleteProperty(globalThis, key);
|
||||
}
|
||||
|
||||
function setupYoutubePickerTestEnv(options?: {
|
||||
windowValue?: YoutubePickerTestWindow;
|
||||
customEventValue?: unknown;
|
||||
@@ -163,41 +152,11 @@ function setupYoutubePickerTestEnv(options?: {
|
||||
};
|
||||
}
|
||||
|
||||
test('youtube picker test env restore deletes injected globals that were originally absent', () => {
|
||||
const previousWindowDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
||||
const previousDocumentDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
|
||||
try {
|
||||
Reflect.deleteProperty(globalThis, 'window');
|
||||
Reflect.deleteProperty(globalThis, 'document');
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'window'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'document'), false);
|
||||
|
||||
const env = setupYoutubePickerTestEnv();
|
||||
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'window'), true);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'document'), true);
|
||||
|
||||
env.restore();
|
||||
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'window'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(globalThis, 'document'), false);
|
||||
assert.equal(typeof globalThis.window, 'undefined');
|
||||
assert.equal(typeof globalThis.document, 'undefined');
|
||||
} finally {
|
||||
restoreGlobalDescriptor('window', previousWindowDescriptor);
|
||||
restoreGlobalDescriptor('document', previousDocumentDescriptor);
|
||||
}
|
||||
});
|
||||
|
||||
test('youtube track picker close restores focus and mouse-ignore state', () => {
|
||||
const overlayFocusCalls: number[] = [];
|
||||
const windowFocusCalls: number[] = [];
|
||||
const focusMainWindowCalls: number[] = [];
|
||||
let overlayFocused = false;
|
||||
const ignoreCalls: Array<{ ignore: boolean; forward?: boolean }> = [];
|
||||
const notifications: string[] = [];
|
||||
const frontendCommands: unknown[] = [];
|
||||
const syncCalls: string[] = [];
|
||||
|
||||
class TestCustomEvent extends Event {
|
||||
detail: unknown;
|
||||
@@ -214,17 +173,13 @@ test('youtube track picker close restores focus and mouse-ignore state', () => {
|
||||
frontendCommands.push(event.detail ?? null);
|
||||
return true;
|
||||
},
|
||||
focus: () => {
|
||||
windowFocusCalls.push(1);
|
||||
},
|
||||
focus: () => {},
|
||||
electronAPI: {
|
||||
notifyOverlayModalOpened: () => {},
|
||||
notifyOverlayModalClosed: (modal: string) => {
|
||||
notifications.push(modal);
|
||||
},
|
||||
focusMainWindow: async () => {
|
||||
focusMainWindowCalls.push(1);
|
||||
},
|
||||
focusMainWindow: async () => {},
|
||||
youtubePickerResolve: async () => ({ ok: true, message: '' }),
|
||||
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
|
||||
ignoreCalls.push({ ignore, forward: options?.forward });
|
||||
@@ -238,7 +193,7 @@ test('youtube track picker close restores focus and mouse-ignore state', () => {
|
||||
const state = createRendererState();
|
||||
const dom = createYoutubePickerDomFixture();
|
||||
dom.overlay.focus = () => {
|
||||
overlayFocusCalls.push(1);
|
||||
overlayFocused = true;
|
||||
};
|
||||
const { overlay } = dom;
|
||||
|
||||
@@ -252,12 +207,8 @@ test('youtube track picker close restores focus and mouse-ignore state', () => {
|
||||
} as never,
|
||||
{
|
||||
modalStateReader: { isAnyModalOpen: () => false },
|
||||
restorePointerInteractionState: () => {
|
||||
syncCalls.push('restore-pointer');
|
||||
},
|
||||
syncSettingsModalSubtitleSuppression: () => {
|
||||
syncCalls.push('sync');
|
||||
},
|
||||
restorePointerInteractionState: () => {},
|
||||
syncSettingsModalSubtitleSuppression: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -272,13 +223,10 @@ test('youtube track picker close restores focus and mouse-ignore state', () => {
|
||||
modal.closeYoutubePickerModal();
|
||||
|
||||
assert.equal(state.youtubePickerModalOpen, false);
|
||||
assert.deepEqual(syncCalls, ['sync', 'sync', 'restore-pointer']);
|
||||
assert.deepEqual(notifications, ['youtube-track-picker']);
|
||||
assert.deepEqual(frontendCommands, [{ type: 'refreshOptions' }]);
|
||||
assert.equal(overlay.classList.contains('interactive'), false);
|
||||
assert.equal(focusMainWindowCalls.length > 0, true);
|
||||
assert.equal(overlayFocusCalls.length > 0, true);
|
||||
assert.equal(windowFocusCalls.length > 0, true);
|
||||
assert.equal(overlayFocused, true);
|
||||
assert.deepEqual(ignoreCalls, [{ ignore: true, forward: true }]);
|
||||
} finally {
|
||||
env.restore();
|
||||
|
||||
Reference in new issue
Block a user