Files
iptvnator/apps/electron-backend-e2e/src/performance/xtream-renderer-probe.spec.ts
T
4grayandClaude Opus 5.5 078b91bfdf test(perf): type-check the whole electron-backend-e2e project (#1869)
* 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>
2026-10-10 14:33:15 +02:00

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);
}
}