Files
SubMiner/src/core/services/field-grouping-overlay.test.ts
T

337 lines
10 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { KikuFieldGroupingChoice } from '../../types';
import { createFieldGroupingOverlayRuntime } from './field-grouping-overlay';
test('createFieldGroupingOverlayRuntime sends overlay messages and sets restore flag', () => {
const sent: unknown[][] = [];
let visible = false;
const restore = new Set<'runtime-options' | 'subsync'>();
const runtime = createFieldGroupingOverlayRuntime<'runtime-options' | 'subsync'>({
getMainWindow: () => ({
isDestroyed: () => false,
webContents: {
isLoading: () => false,
send: (...args: unknown[]) => {
sent.push(args);
},
},
}),
getVisibleOverlayVisible: () => visible,
setVisibleOverlayVisible: (next) => {
visible = next;
},
getResolver: () => null,
setResolver: () => {},
getRestoreVisibleOverlayOnModalClose: () => restore,
});
const ok = runtime.sendToVisibleOverlay('runtime-options:open', undefined, {
restoreOnModalClose: 'runtime-options',
});
assert.equal(ok, true);
assert.equal(visible, true);
assert.equal(restore.has('runtime-options'), true);
assert.deepEqual(sent, [['runtime-options:open']]);
});
test('createFieldGroupingOverlayRuntime callback cancels when send fails', async () => {
let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null;
const runtime = createFieldGroupingOverlayRuntime<'runtime-options' | 'subsync'>({
getMainWindow: () => null,
getVisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
getResolver: () => resolver,
setResolver: (next: ((choice: KikuFieldGroupingChoice) => void) | null) => {
resolver = next;
},
getRestoreVisibleOverlayOnModalClose: () => new Set<'runtime-options' | 'subsync'>(),
});
const callback = runtime.createFieldGroupingCallback();
const result = await callback({
original: {
noteId: 1,
expression: 'a',
sentencePreview: 'a',
hasAudio: false,
hasImage: false,
isOriginal: true,
},
duplicate: {
noteId: 2,
expression: 'b',
sentencePreview: 'b',
hasAudio: false,
hasImage: false,
isOriginal: false,
},
});
assert.equal(result.cancelled, true);
assert.equal(result.keepNoteId, 0);
assert.equal(result.deleteNoteId, 0);
});
test('createFieldGroupingOverlayRuntime callback restores hidden visible overlay after resolver settles', async () => {
let resolver: unknown = null;
let visible = false;
const visibilityTransitions: boolean[] = [];
const runtime = createFieldGroupingOverlayRuntime<'runtime-options' | 'subsync'>({
getMainWindow: () => null,
getVisibleOverlayVisible: () => visible,
setVisibleOverlayVisible: (nextVisible) => {
visible = nextVisible;
visibilityTransitions.push(nextVisible);
},
getResolver: () => resolver as ((choice: KikuFieldGroupingChoice) => void) | null,
setResolver: (nextResolver: ((choice: KikuFieldGroupingChoice) => void) | null) => {
resolver = nextResolver;
},
getRestoreVisibleOverlayOnModalClose: () => new Set<'runtime-options' | 'subsync'>(),
sendToVisibleOverlay: () => true,
});
const callback = runtime.createFieldGroupingCallback();
const pendingChoice = callback({
original: {
noteId: 1,
expression: 'a',
sentencePreview: 'a',
hasAudio: false,
hasImage: false,
isOriginal: true,
},
duplicate: {
noteId: 2,
expression: 'b',
sentencePreview: 'b',
hasAudio: false,
hasImage: false,
isOriginal: false,
},
});
assert.equal(visible, true);
assert.ok(resolver);
if (typeof resolver !== 'function') {
throw new Error('expected field grouping resolver to be assigned');
}
(resolver as (choice: KikuFieldGroupingChoice) => void)({
keepNoteId: 1,
deleteNoteId: 2,
deleteDuplicate: true,
cancelled: false,
});
await pendingChoice;
assert.equal(visible, false);
assert.deepEqual(visibilityTransitions, [true, false]);
});
async function settleWithinMicrotasks<T>(
promise: Promise<T>,
attempts = 10,
): Promise<T | 'timeout'> {
let settled = false;
let settledValue: T | undefined;
void promise.then((value) => {
settled = true;
settledValue = value;
});
for (let i = 0; i < attempts; i += 1) {
await Promise.resolve();
if (settled) {
return settledValue as T;
}
}
return 'timeout';
}
test('createFieldGroupingOverlayRuntime callback cancels and cleans up when kiku modal never acknowledges open', async () => {
let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null;
const sends: Array<{
channel: string;
payload: unknown;
restoreOnModalClose?: string;
preferModalWindow?: boolean;
}> = [];
const waitCalls: Array<{ modal: string; timeoutMs: number }> = [];
const warnings: string[] = [];
const closed: string[] = [];
const originalSetTimeout = globalThis.setTimeout;
globalThis.setTimeout = (() => 0) as unknown as typeof globalThis.setTimeout;
try {
const runtime = createFieldGroupingOverlayRuntime<'kiku'>({
getMainWindow: () => null,
getVisibleOverlayVisible: () => true,
setVisibleOverlayVisible: () => {},
getResolver: () => resolver,
setResolver: (nextResolver) => {
resolver = nextResolver;
},
getRestoreVisibleOverlayOnModalClose: () => new Set<'kiku'>(),
sendToVisibleOverlay: (channel, payload, runtimeOptions) => {
sends.push({
channel,
payload,
restoreOnModalClose: runtimeOptions?.restoreOnModalClose,
preferModalWindow: runtimeOptions?.preferModalWindow,
});
return true;
},
waitForModalOpen: async (modal, timeoutMs) => {
waitCalls.push({ modal, timeoutMs });
return false;
},
handleOverlayModalClosed: (modal) => {
closed.push(modal);
},
logWarn: (message) => {
warnings.push(message);
},
});
const request = {
original: {
noteId: 1,
expression: 'a',
sentencePreview: 'a',
hasAudio: false,
hasImage: false,
isOriginal: true,
},
duplicate: {
noteId: 2,
expression: 'b',
sentencePreview: 'b',
hasAudio: false,
hasImage: false,
isOriginal: false,
},
};
const pendingChoice = runtime.createFieldGroupingCallback()(request);
const result = await settleWithinMicrotasks(pendingChoice);
assert.notEqual(result, 'timeout');
assert.deepEqual(result, {
keepNoteId: 0,
deleteNoteId: 0,
deleteDuplicate: true,
cancelled: true,
});
assert.equal(resolver, null);
assert.deepEqual(
sends.map(({ channel, restoreOnModalClose, preferModalWindow }) => ({
channel,
restoreOnModalClose,
preferModalWindow,
})),
[
{
channel: 'kiku:field-grouping-request',
restoreOnModalClose: 'kiku',
preferModalWindow: true,
},
{
channel: 'kiku:field-grouping-request',
restoreOnModalClose: 'kiku',
preferModalWindow: true,
},
// Abandonment also asks the renderer hosting the modal to close its dialog.
{
channel: 'kiku:field-grouping-cancel',
restoreOnModalClose: undefined,
preferModalWindow: true,
},
],
);
assert.deepEqual(waitCalls, [
{ modal: 'kiku', timeoutMs: 1500 },
{ modal: 'kiku', timeoutMs: 1500 },
]);
assert.deepEqual(warnings, [
'Kiku field grouping modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
]);
// Once from the send-failure path inside sendKikuFieldGroupingRequest, once from the
// callback's abandonment cleanup. The real runtime guards this via the restore set, so
// the duplicate is a harmless no-op.
assert.deepEqual(closed, ['kiku', 'kiku']);
} finally {
globalThis.setTimeout = originalSetTimeout;
}
});
test('createFieldGroupingOverlayRuntime prepares overlay windows before opening the modal', async () => {
// The field grouping modal must run the same prerequisites as every other modal
// (openOverlayHostedModal) so it opens with the overlay runtime ready and the visible overlay
// window present — otherwise on Hyprland it fails to sit above / focus over fullscreen mpv.
const order: string[] = [];
const originalSetTimeout = globalThis.setTimeout;
// The modal acknowledges open below, so the callback stays pending and arms its response
// timeout; stub the timer so no real 90s handle leaks into the test runner.
globalThis.setTimeout = (() => 0) as unknown as typeof globalThis.setTimeout;
try {
const runtime = createFieldGroupingOverlayRuntime<'kiku'>({
getMainWindow: () => null,
getVisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
getResolver: () => null,
setResolver: () => {},
getRestoreVisibleOverlayOnModalClose: () => new Set<'kiku'>(),
ensureOverlayStartupPrereqs: () => order.push('prereqs'),
ensureOverlayWindowsReadyForVisibilityActions: () => order.push('windows-ready'),
sendToVisibleOverlay: (channel) => {
order.push(`send:${channel}`);
return true;
},
waitForModalOpen: async () => {
order.push('wait');
return true;
},
});
// Do not await: an acknowledged modal leaves the choice pending until the user responds.
void runtime.createFieldGroupingCallback()({
original: {
noteId: 1,
expression: 'a',
sentencePreview: 'a',
hasAudio: false,
hasImage: false,
isOriginal: true,
},
duplicate: {
noteId: 2,
expression: 'b',
sentencePreview: 'b',
hasAudio: false,
hasImage: false,
isOriginal: false,
},
});
// Let the async send + modal-open ack chain run.
for (let i = 0; i < 10; i += 1) {
await Promise.resolve();
}
assert.deepEqual(order, [
'prereqs',
'windows-ready',
'send:kiku:field-grouping-request',
'wait',
]);
} finally {
globalThis.setTimeout = originalSetTimeout;
}
});