mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
* test(e2e): fix type errors in four Electron E2E specs - category-management: the poll's message and 15 s timeout were passed to toBe(), which takes one argument, so the poll ran with the default 10 s timeout and message. Pass them to expect.poll(). - settings: evaluate<void, HTMLVideoElement> set the argument type, not the element type; type the callback parameter instead. - remote-control: keep the poll callback's writes visible to the return and fail with a clear error instead of casting null. - downloads: read the dataset key through its index signature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(perf): fix type errors in the benchmark harness None of these changes behaviour for a capture the harness produces today: - cancellation report: a null playlist id on the request record now means unknown, like undefined, instead of matching only null DB records. - main capture: type the worker transport map so its unavailable-reason literals stay narrow. - worker request performance: a parsed capture carries non-null work timestamps; only the unavailable fallback reports null. - Xtream iteration: hand the teardown closure the narrowed app and data directory. - Xtream exact schema: return early when the identity is not an exact record instead of re-checking it three times. - Xtream assembly: reject an unpainted UI explicitly; the null-to-0 comparison already rejected it. - Xtream scenario driver: drop the always-null cancellation observer from prepare; trigger() installs it. - Electron process lifecycle: type the child process by the exit subscription it uses, so a test double need not return a ChildProcess. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(perf): fix type errors in the benchmark harness specs The dynamically imported helpers were checked with assert.equal(typeof fn, 'function'), which does not narrow, so later calls used fn?.(). assert.ok(typeof fn === 'function') narrows them. The rest types fixtures by the module under test, keeps closure-assigned callbacks visible after their callbacks run, and narrows nullable fixture values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(perf): type-check the whole electron-backend-e2e project Widen the spec config's include from the journey harness to src/**/*.ts and the root Playwright configs, with no baseline or ignore list, and update the validation map and the journey checklist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(perf): build a complete renderer fixture in the request-performance spec The fixture was cast through unknown, so the type check accepted an incomplete RendererCaptureMetrics. Fill every required field with neutral values; the summary reads only the probe arrays and heap fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
263 lines
8.8 KiB
TypeScript
263 lines
8.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import test from 'node:test';
|
|
|
|
import { XTREAM_SCENARIO_ID } from './xtream-benchmark-contract';
|
|
|
|
interface ProbeModule {
|
|
XTREAM_RENDERER_STATE_KEY?: string;
|
|
assertCompleteXtreamRendererProbe?: (value: unknown) => void;
|
|
assertXtreamBackgroundDeleteActive?: (page: unknown) => Promise<void>;
|
|
assertXtreamBackgroundRefreshActive?: (
|
|
page: unknown,
|
|
identity: {
|
|
readonly operationId: string;
|
|
readonly playlistId: string;
|
|
}
|
|
) => Promise<void>;
|
|
beginXtreamRendererOperation?: (page: unknown) => Promise<number>;
|
|
installXtreamRendererProbe?: (
|
|
page: unknown,
|
|
options: {
|
|
readonly scenarioId: string;
|
|
readonly sourceTitle: string;
|
|
}
|
|
) => Promise<void>;
|
|
recordXtreamRendererHeartbeat?: (
|
|
page: unknown,
|
|
deadlineEpochMs: number
|
|
) => Promise<number>;
|
|
stopXtreamRendererProbe?: (page: unknown) => Promise<unknown>;
|
|
waitForXtreamRendererTerminal?: (page: unknown) => Promise<void>;
|
|
}
|
|
|
|
const probeUrl = new URL('./xtream-renderer-probe.ts', import.meta.url);
|
|
const modulePromise = import(probeUrl.href)
|
|
.then((module) => module as unknown as ProbeModule)
|
|
.catch(() => null);
|
|
|
|
test('exports the fail-closed Xtream renderer probe lifecycle', async () => {
|
|
const module = await modulePromise;
|
|
|
|
assert.ok(module, 'Xtream renderer probe module must exist');
|
|
assert.equal(typeof module.XTREAM_RENDERER_STATE_KEY, 'string');
|
|
assert.equal(typeof module.installXtreamRendererProbe, 'function');
|
|
assert.equal(typeof module.beginXtreamRendererOperation, 'function');
|
|
assert.equal(typeof module.recordXtreamRendererHeartbeat, 'function');
|
|
assert.equal(typeof module.waitForXtreamRendererTerminal, 'function');
|
|
assert.equal(typeof module.stopXtreamRendererProbe, 'function');
|
|
assert.equal(typeof module.assertCompleteXtreamRendererProbe, 'function');
|
|
assert.equal(typeof module.assertXtreamBackgroundDeleteActive, 'function');
|
|
assert.equal(typeof module.assertXtreamBackgroundRefreshActive, 'function');
|
|
});
|
|
|
|
test('passively correlates the driver-owned cancel click with live DB progress', () => {
|
|
const source = readFileSync(probeUrl, 'utf8');
|
|
|
|
assert.match(source, /RENDERER_PERFORMANCE_PHASE_HOOK_KEY/);
|
|
assert.match(source, /onDbOperationEvent/);
|
|
assert.doesNotMatch(source, /event\.error/);
|
|
assert.match(source, /Math\.max\(\s*100,[\s\S]*Math\.floor/);
|
|
assert.match(source, /workspace-loading-overlay__action/);
|
|
assert.match(source, /addEventListener\('click'/);
|
|
assert.doesNotMatch(source, /\.click\(\)/);
|
|
assert.match(source, /Math\.min\(/);
|
|
assert.match(source, /requestAnimationFrame/);
|
|
assert.match(source, /longtask/);
|
|
});
|
|
|
|
test('requires a final store marker, DOM gate, and two rendered frames', () => {
|
|
const source = readFileSync(probeUrl, 'utf8');
|
|
|
|
assert.match(source, /store\.xtream-import-terminal/);
|
|
assert.match(source, /routeHydratesAfterImport/);
|
|
assert.match(source, /requiredStoreTerminalCount/);
|
|
assert.match(source, /routeHydratesAfterImport\s*\?\s*2\s*:\s*1/);
|
|
assert.match(source, /store\.xtream-delete-row/);
|
|
assert.match(source, /workspace-loading-overlay/);
|
|
assert.match(source, /xtream-content-gate/);
|
|
assert.match(source, /catalog/);
|
|
assert.match(source, /paintFrameCount/);
|
|
assert.match(source, />=\s*2/);
|
|
});
|
|
|
|
test('records every elapsed fixed-grid heartbeat deadline in one renderer delivery', async () => {
|
|
const module = await modulePromise;
|
|
assert.ok(module?.recordXtreamRendererHeartbeat);
|
|
assert.ok(module.XTREAM_RENDERER_STATE_KEY);
|
|
const performanceDescriptor = Object.getOwnPropertyDescriptor(
|
|
globalThis,
|
|
'performance'
|
|
);
|
|
const target = globalThis as unknown as Record<string, unknown>;
|
|
const stateKey = module.XTREAM_RENDERER_STATE_KEY;
|
|
const page = {
|
|
evaluate: async (
|
|
callback: (argument: unknown) => unknown,
|
|
argument: unknown
|
|
) => callback(argument),
|
|
};
|
|
target[stateKey] = {
|
|
heartbeatDelaysMs: [],
|
|
operationStartEpochMs: 100,
|
|
terminalEpochMs: null,
|
|
};
|
|
Object.defineProperty(globalThis, 'performance', {
|
|
configurable: true,
|
|
value: { now: () => 251, timeOrigin: 0 },
|
|
});
|
|
|
|
try {
|
|
const nextDeadline = await module.recordXtreamRendererHeartbeat(
|
|
page,
|
|
150
|
|
);
|
|
assert.equal(nextDeadline, 300);
|
|
assert.deepEqual(
|
|
(
|
|
target[stateKey] as {
|
|
heartbeatDelaysMs: number[];
|
|
}
|
|
).heartbeatDelaysMs,
|
|
[101, 51, 1]
|
|
);
|
|
} finally {
|
|
delete target[stateKey];
|
|
restoreGlobal('performance', performanceDescriptor);
|
|
}
|
|
});
|
|
|
|
test('rejects incomplete and internally inconsistent terminal evidence', async () => {
|
|
const module = await modulePromise;
|
|
assert.ok(module?.assertCompleteXtreamRendererProbe);
|
|
const valid = completeProbe();
|
|
|
|
assert.doesNotThrow(() =>
|
|
module.assertCompleteXtreamRendererProbe?.(valid)
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
module.assertCompleteXtreamRendererProbe?.({
|
|
...valid,
|
|
terminalEpochMs: null,
|
|
}),
|
|
/xtream-renderer-probe-incomplete/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
module.assertCompleteXtreamRendererProbe?.({
|
|
...valid,
|
|
uiPaintedEpochMs: 90,
|
|
}),
|
|
/xtream-renderer-probe-incomplete/
|
|
);
|
|
});
|
|
|
|
test('keeps a completed delete inside the identified refresh lifecycle until store terminal', async () => {
|
|
const module = await modulePromise;
|
|
const assertRefreshActive = module?.assertXtreamBackgroundRefreshActive;
|
|
assert.ok(assertRefreshActive);
|
|
assert.ok(module?.XTREAM_RENDERER_STATE_KEY);
|
|
const stateKey = module.XTREAM_RENDERER_STATE_KEY;
|
|
const target = globalThis as unknown as Record<string, unknown>;
|
|
const identity = {
|
|
operationId: 'delete-operation-1',
|
|
playlistId: 'playlist-1',
|
|
};
|
|
const page = {
|
|
evaluate: async (
|
|
callback: (argument: unknown) => unknown,
|
|
argument: unknown
|
|
) => callback(argument),
|
|
};
|
|
target[stateKey] = backgroundRefreshState(identity);
|
|
try {
|
|
await assert.doesNotReject(() => assertRefreshActive(page, identity));
|
|
target[stateKey] = {
|
|
...backgroundRefreshState(identity),
|
|
storeTerminalEpochMs: 200,
|
|
};
|
|
await assert.rejects(
|
|
() => assertRefreshActive(page, identity),
|
|
/xtream-background-refresh-not-active/
|
|
);
|
|
target[stateKey] = backgroundRefreshState({
|
|
...identity,
|
|
operationId: 'different-operation',
|
|
});
|
|
await assert.rejects(
|
|
() => assertRefreshActive(page, identity),
|
|
/xtream-background-refresh-not-active/
|
|
);
|
|
} finally {
|
|
delete target[stateKey];
|
|
}
|
|
});
|
|
|
|
function completeProbe(): Record<string, unknown> {
|
|
return {
|
|
cancellationClickEpochMs: null,
|
|
cancelEligibleProgressEpochMs: null,
|
|
dbCancellationTerminalEpochMs: null,
|
|
dbOperationEvents: [],
|
|
domReadyEpochMs: 150,
|
|
frameGapsMs: [16],
|
|
heartbeatDelaysMs: [2],
|
|
invalidReason: null,
|
|
longTasksMs: [],
|
|
operationStartEpochMs: 100,
|
|
performancePhaseEvents: [
|
|
{
|
|
boundary: 'end',
|
|
epochMs: 140,
|
|
monotonicMs: 40,
|
|
outcome: 'success',
|
|
phase: 'store.xtream-import-terminal',
|
|
phaseId: 'terminal-1',
|
|
schemaVersion: 1,
|
|
},
|
|
],
|
|
routeReadyEpochMs: 120,
|
|
scenarioId: XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
|
storeTerminalEpochMs: 140,
|
|
terminalEpochMs: 160,
|
|
uiPaintedEpochMs: 160,
|
|
};
|
|
}
|
|
|
|
function backgroundRefreshState(identity: {
|
|
readonly operationId: string;
|
|
readonly playlistId: string;
|
|
}): Record<string, unknown> {
|
|
return {
|
|
dbOperationEvents: [
|
|
{
|
|
operation: 'delete-xtream-content',
|
|
...identity,
|
|
status: 'started',
|
|
},
|
|
{
|
|
operation: 'delete-xtream-content',
|
|
...identity,
|
|
status: 'completed',
|
|
},
|
|
],
|
|
invalidReason: null,
|
|
operationStartEpochMs: 100,
|
|
scenarioId: XTREAM_SCENARIO_ID.BACKGROUND_UI,
|
|
storeTerminalEpochMs: null,
|
|
terminalEpochMs: null,
|
|
};
|
|
}
|
|
|
|
function restoreGlobal(
|
|
key: string,
|
|
descriptor: PropertyDescriptor | undefined
|
|
): void {
|
|
if (descriptor) {
|
|
Object.defineProperty(globalThis, key, descriptor);
|
|
} else {
|
|
Reflect.deleteProperty(globalThis, key);
|
|
}
|
|
}
|