mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
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>
This commit is contained in:
1 parent
6e18983400
commit
078b91bfdf
36 files changed
+229
-165
No files matched your search
@@ -619,38 +619,43 @@ async function pickSidebarCategory(
|
||||
} | null = null;
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const candidates = (
|
||||
await readVisibleSidebarCategories(page)
|
||||
).filter(
|
||||
(candidate) =>
|
||||
candidate.id && candidate.name && candidate.itemCount > 0
|
||||
);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
preferredCandidate = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
const nameCounts = new Map<string, number>();
|
||||
for (const candidate of candidates) {
|
||||
nameCounts.set(
|
||||
candidate.name,
|
||||
(nameCounts.get(candidate.name) ?? 0) + 1
|
||||
.poll(
|
||||
async () => {
|
||||
const candidates = (
|
||||
await readVisibleSidebarCategories(page)
|
||||
).filter(
|
||||
(candidate) =>
|
||||
candidate.id &&
|
||||
candidate.name &&
|
||||
candidate.itemCount > 0
|
||||
);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
preferredCandidate = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
const nameCounts = new Map<string, number>();
|
||||
for (const candidate of candidates) {
|
||||
nameCounts.set(
|
||||
candidate.name,
|
||||
(nameCounts.get(candidate.name) ?? 0) + 1
|
||||
);
|
||||
}
|
||||
|
||||
preferredCandidate =
|
||||
candidates.find(
|
||||
(candidate) => nameCounts.get(candidate.name) === 1
|
||||
) ?? candidates[0];
|
||||
|
||||
return preferredCandidate !== null;
|
||||
},
|
||||
{
|
||||
message: 'No visible Xtream category with content was found.',
|
||||
timeout: 15000,
|
||||
}
|
||||
|
||||
preferredCandidate =
|
||||
candidates.find(
|
||||
(candidate) => nameCounts.get(candidate.name) === 1
|
||||
) ?? candidates[0];
|
||||
|
||||
return preferredCandidate !== null;
|
||||
})
|
||||
.toBe(true, {
|
||||
message: 'No visible Xtream category with content was found.',
|
||||
timeout: 15000,
|
||||
});
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
return preferredCandidate!;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ async function selectDownloadCoverSize(
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
() => document.documentElement.dataset.coverSize ?? null
|
||||
() => document.documentElement.dataset['coverSize'] ?? null
|
||||
)
|
||||
)
|
||||
.toBe(size);
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { terminateElectronProcess } from './electron-process-termination';
|
||||
|
||||
type ElectronChildProcess = Pick<
|
||||
// Only the exit subscription is used; picking ChildProcess's own once and
|
||||
// removeListener would demand that every listener host returns a ChildProcess.
|
||||
interface ElectronChildProcess extends Pick<
|
||||
ChildProcess,
|
||||
'exitCode' | 'kill' | 'once' | 'pid' | 'removeListener' | 'signalCode'
|
||||
>;
|
||||
'exitCode' | 'kill' | 'pid' | 'signalCode'
|
||||
> {
|
||||
once(event: 'exit', listener: () => void): unknown;
|
||||
removeListener(event: 'exit', listener: () => void): unknown;
|
||||
}
|
||||
|
||||
export interface ClosableElectronApplication {
|
||||
close(): Promise<void>;
|
||||
|
||||
+7
-2
@@ -6,6 +6,11 @@ import {
|
||||
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON,
|
||||
} from './database-worker-peak-memory-validity';
|
||||
|
||||
type PeakMemoryIteration = Parameters<
|
||||
typeof assessDatabaseWorkerPeakMemoryValidity
|
||||
>[0][number];
|
||||
type PeakMemoryWorker = PeakMemoryIteration['main']['workers'][number];
|
||||
|
||||
describe('database worker peak-memory validity', () => {
|
||||
it('requires successful heap and external-memory samples in every measured run', () => {
|
||||
const validity = assessDatabaseWorkerPeakMemoryValidity([
|
||||
@@ -96,8 +101,8 @@ describe('database worker peak-memory validity', () => {
|
||||
function iteration(
|
||||
runId: string,
|
||||
kind: string,
|
||||
databaseWorker: Record<string, unknown>
|
||||
) {
|
||||
databaseWorker: PeakMemoryWorker
|
||||
): PeakMemoryIteration {
|
||||
return {
|
||||
kind,
|
||||
main: { workers: [databaseWorker] },
|
||||
|
||||
@@ -31,7 +31,7 @@ async function restoreSerializableApi(): Promise<CutoffApi> {
|
||||
const module = await cutoffModulePromise;
|
||||
assert.ok(module, 'database worker post-GC cutoff module must exist');
|
||||
const factory = module.createDatabaseWorkerPostGcCutoffApi;
|
||||
assert.equal(typeof factory, 'function');
|
||||
assert.ok(typeof factory === 'function');
|
||||
|
||||
const source = factory.toString();
|
||||
assert.doesNotMatch(source, /__name/);
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ async function restoreSerializableApi(): Promise<FinalizationApi> {
|
||||
const module = await finalizationModulePromise;
|
||||
assert.ok(module, 'database worker post-GC finalization module must exist');
|
||||
const factory = module.createDatabaseWorkerPostGcFinalizationApi;
|
||||
assert.equal(typeof factory, 'function');
|
||||
assert.ok(typeof factory === 'function');
|
||||
|
||||
const source = factory.toString();
|
||||
assert.doesNotMatch(source, /__name/);
|
||||
|
||||
@@ -158,7 +158,7 @@ async function restoreSerializableApi(): Promise<ProbeApi> {
|
||||
const module = await probeModulePromise;
|
||||
assert.ok(module, 'database worker post-GC probe module must exist');
|
||||
const factory = module.createDatabaseWorkerPostGcProbeApi;
|
||||
assert.equal(typeof factory, 'function');
|
||||
assert.ok(typeof factory === 'function');
|
||||
|
||||
const source = factory.toString();
|
||||
assert.doesNotMatch(source, /__name/);
|
||||
@@ -351,18 +351,20 @@ test('times out once with an injectable deadline and ignores every later termina
|
||||
const api = await restoreSerializableApi();
|
||||
const harness = createProbeHarness();
|
||||
let terminateCalls = 0;
|
||||
// A real worker also exposes terminate(); the probe must never call it.
|
||||
const worker = {
|
||||
postMessage(): void {
|
||||
// Keep the one-shot probe pending until its bounded deadline.
|
||||
},
|
||||
terminate(): void {
|
||||
terminateCalls += 1;
|
||||
},
|
||||
};
|
||||
const resultPromise = api.probe({
|
||||
createMessageChannel: harness.createMessageChannel,
|
||||
timeoutMs: 37,
|
||||
timers: harness.timers,
|
||||
worker: {
|
||||
postMessage(): void {
|
||||
// Keep the one-shot probe pending until its bounded deadline.
|
||||
},
|
||||
terminate(): void {
|
||||
terminateCalls += 1;
|
||||
},
|
||||
},
|
||||
worker,
|
||||
});
|
||||
const timer = harness.timers.scheduled[0];
|
||||
assert.ok(timer);
|
||||
|
||||
@@ -59,7 +59,7 @@ async function restoreSerializableSelector(): Promise<Selector> {
|
||||
const module = await selectionModulePromise;
|
||||
assert.ok(module, 'database worker post-GC selector module must exist');
|
||||
const factory = module.createDatabaseWorkerPostGcSelectionApi;
|
||||
assert.equal(typeof factory, 'function');
|
||||
assert.ok(typeof factory === 'function');
|
||||
const source = factory.toString();
|
||||
assert.doesNotMatch(source, /__name/);
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ test('validates database post-GC heap per measured iteration without losing raw
|
||||
const module = await validityModulePromise;
|
||||
assert.ok(module, 'database worker post-GC validity helper must exist');
|
||||
const assess = module.assessDatabaseWorkerPostGcValidity;
|
||||
assert.equal(typeof assess, 'function');
|
||||
assert.ok(typeof assess === 'function');
|
||||
|
||||
const valid = assess([
|
||||
iteration('warmup-broken', 'warmup', []),
|
||||
@@ -183,7 +183,7 @@ test('marks parsing cancellation before persistence as DB N/A without accepting
|
||||
const module = await validityModulePromise;
|
||||
assert.ok(module);
|
||||
const assess = module.assessDatabaseWorkerPostGcValidity;
|
||||
assert.equal(typeof assess, 'function');
|
||||
assert.ok(typeof assess === 'function');
|
||||
|
||||
const noDatabasePhase = assess([
|
||||
iteration(
|
||||
|
||||
+8
-8
@@ -52,7 +52,7 @@ test('accepts one parseable diagnostic playlist-worker profile inside the iterat
|
||||
'diagnostic playlist worker capture validator must exist'
|
||||
);
|
||||
const validate = module.validateDiagnosticPlaylistWorkerCapture;
|
||||
assert.equal(typeof validate, 'function');
|
||||
assert.ok(typeof validate === 'function');
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-diagnostic-worker-')
|
||||
);
|
||||
@@ -67,7 +67,7 @@ test('accepts one parseable diagnostic playlist-worker profile inside the iterat
|
||||
JSON.stringify({ nodes: [{ id: 1 }], samples: [1] })
|
||||
);
|
||||
await assert.doesNotReject(() =>
|
||||
validate?.(mainCapture({ profilePath }), directory)
|
||||
validate(mainCapture({ profilePath }), directory)
|
||||
);
|
||||
} finally {
|
||||
await rm(directory, { force: true, recursive: true });
|
||||
@@ -78,19 +78,19 @@ test('fails closed for missing, escaped, malformed, or contaminated diagnostic a
|
||||
const module = await diagnosticCaptureModulePromise;
|
||||
assert.ok(module);
|
||||
const validate = module.validateDiagnosticPlaylistWorkerCapture;
|
||||
assert.equal(typeof validate, 'function');
|
||||
assert.ok(typeof validate === 'function');
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-diagnostic-worker-invalid-')
|
||||
);
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
() => validate?.(mainCapture({ profilePath: null }), directory),
|
||||
() => validate(mainCapture({ profilePath: null }), directory),
|
||||
/diagnostic-playlist-worker-profile-missing/
|
||||
);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validate?.(
|
||||
validate(
|
||||
mainCapture({
|
||||
profilePath: join(
|
||||
directory,
|
||||
@@ -109,7 +109,7 @@ test('fails closed for missing, escaped, malformed, or contaminated diagnostic a
|
||||
await writeFile(malformedPath, '{"nodes":[]}');
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validate?.(
|
||||
validate(
|
||||
mainCapture({ profilePath: malformedPath }),
|
||||
directory
|
||||
),
|
||||
@@ -117,7 +117,7 @@ test('fails closed for missing, escaped, malformed, or contaminated diagnostic a
|
||||
);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validate?.(
|
||||
validate(
|
||||
mainCapture({
|
||||
profilePath: malformedPath,
|
||||
timelineType:
|
||||
@@ -129,7 +129,7 @@ test('fails closed for missing, escaped, malformed, or contaminated diagnostic a
|
||||
);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validate?.(
|
||||
validate(
|
||||
mainCapture({
|
||||
profilePath: malformedPath,
|
||||
snapshotPath: join(
|
||||
|
||||
+20
-20
@@ -137,7 +137,7 @@ test('accepts the complete parseable initial-import diagnostic artifact set', as
|
||||
const module = await diagnosticArtifactsModulePromise;
|
||||
assert.ok(module, 'initial-import diagnostic validator must exist');
|
||||
const validate = module.validateInitialImportDiagnosticArtifacts;
|
||||
assert.equal(typeof validate, 'function');
|
||||
assert.ok(typeof validate === 'function');
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-initial-import-diagnostic-')
|
||||
);
|
||||
@@ -145,7 +145,7 @@ test('accepts the complete parseable initial-import diagnostic artifact set', as
|
||||
try {
|
||||
await writeValidArtifacts(directory);
|
||||
await assert.doesNotReject(() =>
|
||||
validate?.(capture(directory), directory)
|
||||
validate(capture(directory), directory)
|
||||
);
|
||||
} finally {
|
||||
await rm(directory, { force: true, recursive: true });
|
||||
@@ -156,7 +156,7 @@ test('fails closed for malformed or missing required process artifacts', async (
|
||||
const module = await diagnosticArtifactsModulePromise;
|
||||
assert.ok(module);
|
||||
const validate = module.validateInitialImportDiagnosticArtifacts;
|
||||
assert.equal(typeof validate, 'function');
|
||||
assert.ok(typeof validate === 'function');
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-initial-import-invalid-')
|
||||
);
|
||||
@@ -165,7 +165,7 @@ test('fails closed for malformed or missing required process artifacts', async (
|
||||
await writeValidArtifacts(directory);
|
||||
await writeFile(join(directory, 'renderer.trace.json'), '{');
|
||||
await assert.rejects(
|
||||
() => validate?.(capture(directory), directory),
|
||||
() => validate(capture(directory), directory),
|
||||
/initial-import-diagnostic-artifact-invalid:renderer-trace/
|
||||
);
|
||||
|
||||
@@ -181,7 +181,7 @@ test('fails closed for malformed or missing required process artifacts', async (
|
||||
},
|
||||
};
|
||||
await assert.rejects(
|
||||
() => validate?.(missingMainProfile, directory),
|
||||
() => validate(missingMainProfile, directory),
|
||||
/initial-import-diagnostic-artifact-missing:main-cpu-profile/
|
||||
);
|
||||
} finally {
|
||||
@@ -193,7 +193,7 @@ test('rejects escaped paths and symlinks instead of reading outside the iteratio
|
||||
const module = await diagnosticArtifactsModulePromise;
|
||||
assert.ok(module);
|
||||
const validate = module.validateInitialImportDiagnosticArtifacts;
|
||||
assert.equal(typeof validate, 'function');
|
||||
assert.ok(typeof validate === 'function');
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-initial-import-contained-')
|
||||
);
|
||||
@@ -215,7 +215,7 @@ test('rejects escaped paths and symlinks instead of reading outside the iteratio
|
||||
JSON.stringify(CPU_PROFILE)
|
||||
);
|
||||
await assert.rejects(
|
||||
() => validate?.(escapedCapture, directory),
|
||||
() => validate(escapedCapture, directory),
|
||||
/initial-import-diagnostic-artifact-path-invalid:main-cpu-profile/
|
||||
);
|
||||
|
||||
@@ -226,7 +226,7 @@ test('rejects escaped paths and symlinks instead of reading outside the iteratio
|
||||
rendererProfilePath
|
||||
);
|
||||
await assert.rejects(
|
||||
() => validate?.(capture(directory), directory),
|
||||
() => validate(capture(directory), directory),
|
||||
/initial-import-diagnostic-artifact-path-invalid:renderer-cpu-profile/
|
||||
);
|
||||
} finally {
|
||||
@@ -239,7 +239,7 @@ test('requires exactly one database profile and rejects every playlist-worker pr
|
||||
const module = await diagnosticArtifactsModulePromise;
|
||||
assert.ok(module);
|
||||
const validate = module.validateInitialImportDiagnosticArtifacts;
|
||||
assert.equal(typeof validate, 'function');
|
||||
assert.ok(typeof validate === 'function');
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-initial-import-workers-')
|
||||
);
|
||||
@@ -251,7 +251,7 @@ test('requires exactly one database profile and rejects every playlist-worker pr
|
||||
JSON.stringify(CPU_PROFILE)
|
||||
);
|
||||
await assert.rejects(
|
||||
() => validate?.(capture(directory), directory),
|
||||
() => validate(capture(directory), directory),
|
||||
/initial-import-diagnostic-database-profile-cardinality-invalid/
|
||||
);
|
||||
|
||||
@@ -261,7 +261,7 @@ test('requires exactly one database profile and rejects every playlist-worker pr
|
||||
JSON.stringify(CPU_PROFILE)
|
||||
);
|
||||
await assert.rejects(
|
||||
() => validate?.(capture(directory), directory),
|
||||
() => validate(capture(directory), directory),
|
||||
/initial-import-diagnostic-playlist-profile-unexpected/
|
||||
);
|
||||
} finally {
|
||||
@@ -273,7 +273,7 @@ test('requires a structurally valid database worker heap snapshot', async () =>
|
||||
const module = await diagnosticArtifactsModulePromise;
|
||||
assert.ok(module);
|
||||
const validate = module.validateInitialImportDiagnosticArtifacts;
|
||||
assert.equal(typeof validate, 'function');
|
||||
assert.ok(typeof validate === 'function');
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-initial-import-worker-snapshot-')
|
||||
);
|
||||
@@ -293,7 +293,7 @@ test('requires a structurally valid database worker heap snapshot', async () =>
|
||||
},
|
||||
};
|
||||
await assert.rejects(
|
||||
() => validate?.(missingSnapshot, directory),
|
||||
() => validate(missingSnapshot, directory),
|
||||
/initial-import-diagnostic-artifact-missing:database-heap-snapshot/
|
||||
);
|
||||
|
||||
@@ -302,7 +302,7 @@ test('requires a structurally valid database worker heap snapshot', async () =>
|
||||
JSON.stringify({ snapshot: {}, nodes: [], edges: [] })
|
||||
);
|
||||
await assert.rejects(
|
||||
() => validate?.(capture(directory), directory),
|
||||
() => validate(capture(directory), directory),
|
||||
/initial-import-diagnostic-artifact-invalid:database-heap-snapshot/
|
||||
);
|
||||
} finally {
|
||||
@@ -314,7 +314,7 @@ test('rejects escaped and symlinked database worker heap snapshots', async () =>
|
||||
const module = await diagnosticArtifactsModulePromise;
|
||||
assert.ok(module);
|
||||
const validate = module.validateInitialImportDiagnosticArtifacts;
|
||||
assert.equal(typeof validate, 'function');
|
||||
assert.ok(typeof validate === 'function');
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-initial-import-worker-contained-')
|
||||
);
|
||||
@@ -342,7 +342,7 @@ test('rejects escaped and symlinked database worker heap snapshots', async () =>
|
||||
},
|
||||
};
|
||||
await assert.rejects(
|
||||
() => validate?.(escapedSnapshot, directory),
|
||||
() => validate(escapedSnapshot, directory),
|
||||
/initial-import-diagnostic-artifact-path-invalid:database-heap-snapshot/
|
||||
);
|
||||
|
||||
@@ -350,7 +350,7 @@ test('rejects escaped and symlinked database worker heap snapshots', async () =>
|
||||
await unlink(snapshotPath);
|
||||
await symlink(outsideSnapshotPath, snapshotPath);
|
||||
await assert.rejects(
|
||||
() => validate?.(capture(directory), directory),
|
||||
() => validate(capture(directory), directory),
|
||||
/initial-import-diagnostic-artifact-path-invalid:database-heap-snapshot/
|
||||
);
|
||||
} finally {
|
||||
@@ -363,7 +363,7 @@ test('rejects extra database snapshots and every playlist-worker snapshot', asyn
|
||||
const module = await diagnosticArtifactsModulePromise;
|
||||
assert.ok(module);
|
||||
const validate = module.validateInitialImportDiagnosticArtifacts;
|
||||
assert.equal(typeof validate, 'function');
|
||||
assert.ok(typeof validate === 'function');
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-initial-import-worker-snapshot-count-')
|
||||
);
|
||||
@@ -375,7 +375,7 @@ test('rejects extra database snapshots and every playlist-worker snapshot', asyn
|
||||
JSON.stringify(HEAP_SNAPSHOT)
|
||||
);
|
||||
await assert.rejects(
|
||||
() => validate?.(capture(directory), directory),
|
||||
() => validate(capture(directory), directory),
|
||||
/initial-import-diagnostic-database-snapshot-cardinality-invalid/
|
||||
);
|
||||
|
||||
@@ -385,7 +385,7 @@ test('rejects extra database snapshots and every playlist-worker snapshot', asyn
|
||||
JSON.stringify(HEAP_SNAPSHOT)
|
||||
);
|
||||
await assert.rejects(
|
||||
() => validate?.(capture(directory), directory),
|
||||
() => validate(capture(directory), directory),
|
||||
/initial-import-diagnostic-playlist-snapshot-unexpected/
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -335,9 +335,9 @@ test('fails closed when any terminal proof or the probe itself is missing', asyn
|
||||
const module = await captureModulePromise;
|
||||
assert.ok(module);
|
||||
const assertComplete = module.assertCompleteM3uImportRendererProbe;
|
||||
assert.equal(typeof assertComplete, 'function');
|
||||
assert.ok(typeof assertComplete === 'function');
|
||||
|
||||
assert.doesNotThrow(() => assertComplete?.(completeProbe()));
|
||||
assert.doesNotThrow(() => assertComplete(completeProbe()));
|
||||
for (const field of [
|
||||
'playlistId',
|
||||
'routeReadyEpochMs',
|
||||
@@ -347,13 +347,13 @@ test('fails closed when any terminal proof or the probe itself is missing', asyn
|
||||
] as const) {
|
||||
const incomplete = { ...completeProbe(), [field]: null };
|
||||
assert.throws(
|
||||
() => assertComplete?.(incomplete),
|
||||
() => assertComplete(incomplete),
|
||||
/m3u-import-renderer-probe-incomplete/
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() =>
|
||||
assertComplete?.({
|
||||
assertComplete({
|
||||
...completeProbe(),
|
||||
operationStartEpochMs: 0,
|
||||
}),
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ test('waits for an in-flight heartbeat and flushes its next deadline once', asyn
|
||||
'document',
|
||||
'HTMLButtonElement',
|
||||
]);
|
||||
let scheduledHeartbeat: (() => void) | null = null;
|
||||
let scheduledHeartbeat = null as (() => void) | null;
|
||||
let releaseHeartbeat!: () => void;
|
||||
const heartbeatGate = new Promise<void>((resolve) => {
|
||||
releaseHeartbeat = resolve;
|
||||
|
||||
@@ -329,7 +329,7 @@ function derivePhases(
|
||||
);
|
||||
const response = findTimeline(main.timeline, 'playlist-response');
|
||||
const playlistRequest = findTimeline(main.timeline, 'playlist-request');
|
||||
const playlistId = playlistRequest?.playlistId;
|
||||
const playlistId = playlistRequest?.playlistId ?? undefined;
|
||||
const dbGetRequest = findPlaylistDbRecord(
|
||||
main.timeline,
|
||||
'db-request',
|
||||
|
||||
@@ -205,6 +205,8 @@ export interface XtreamMainCaptureResult {
|
||||
}
|
||||
|
||||
type MainCaptureStopTransport = XtreamMainCaptureTransport;
|
||||
type MainCaptureWorkerTransport =
|
||||
MainCaptureGenerationTransport['workers'][number];
|
||||
|
||||
export async function installMainCapture(
|
||||
electronApp: ElectronApplication
|
||||
@@ -1897,7 +1899,7 @@ export async function installMainCapture(
|
||||
record.kind === 'database.worker' ||
|
||||
record.kind === 'playlist-refresh.worker'
|
||||
)
|
||||
.map((record) => ({
|
||||
.map((record): MainCaptureWorkerTransport => ({
|
||||
captureGeneration: record.captureGeneration,
|
||||
metrics: {
|
||||
cancelPostedEpochMs: record.cancelPostedEpochMs,
|
||||
|
||||
@@ -43,7 +43,7 @@ function readProject(relativePath: string): ProjectConfiguration {
|
||||
}
|
||||
|
||||
function readResolvedWebBuildTask(): NxGraphTask | undefined {
|
||||
const environment = {
|
||||
const environment: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
FORCE_COLOR: '0',
|
||||
NX_DAEMON: 'false',
|
||||
|
||||
@@ -77,7 +77,7 @@ test('validates only measured renderer RSS runs and preserves the exact summary
|
||||
const module = await validityModulePromise;
|
||||
assert.ok(module, 'generic renderer RSS validity helper must exist');
|
||||
const assess = module.assessRendererRssValidity;
|
||||
assert.equal(typeof assess, 'function');
|
||||
assert.ok(typeof assess === 'function');
|
||||
|
||||
const result = assess([
|
||||
iteration('warmup-invalid', 'warmup', { identity: null }),
|
||||
@@ -102,7 +102,7 @@ test('fails every incomplete measured capture without losing its raw reason and
|
||||
const module = await validityModulePromise;
|
||||
assert.ok(module);
|
||||
const assess = module.assessRendererRssValidity;
|
||||
assert.equal(typeof assess, 'function');
|
||||
assert.ok(typeof assess === 'function');
|
||||
|
||||
const result = assess([
|
||||
iteration('run-no-identity', 'measured', { identity: null }),
|
||||
@@ -147,7 +147,7 @@ test('requires at least one measured run', async () => {
|
||||
const module = await validityModulePromise;
|
||||
assert.ok(module);
|
||||
const assess = module.assessRendererRssValidity;
|
||||
assert.equal(typeof assess, 'function');
|
||||
assert.ok(typeof assess === 'function');
|
||||
|
||||
assert.deepEqual(assess([iteration('warmup-01', 'warmup')]), {
|
||||
invalidMeasuredRuns: [],
|
||||
|
||||
@@ -98,7 +98,7 @@ async function restoreSerializableFactory(): Promise<RendererWindowRssSessionFac
|
||||
const module = await sessionModulePromise;
|
||||
assert.ok(module, 'renderer window RSS session module must exist');
|
||||
const factory = module.createRendererWindowRssSessionApi;
|
||||
assert.equal(typeof factory, 'function');
|
||||
assert.ok(typeof factory === 'function');
|
||||
|
||||
const source = factory.toString();
|
||||
const restoredFactory = Function(
|
||||
|
||||
@@ -6,6 +6,7 @@ import test from 'node:test';
|
||||
import {
|
||||
type CancellationBenchmarkManifest,
|
||||
type CancellationIterationResult,
|
||||
type NumericDistribution,
|
||||
PERFORMANCE_ITERATION_KIND,
|
||||
PERFORMANCE_WORKER_KIND,
|
||||
type WorkerCaptureMetrics,
|
||||
@@ -120,6 +121,16 @@ function playlistWorker(
|
||||
};
|
||||
}
|
||||
|
||||
const EMPTY_DISTRIBUTION: NumericDistribution = {
|
||||
count: 0,
|
||||
max: null,
|
||||
mean: null,
|
||||
median: null,
|
||||
min: null,
|
||||
p95: null,
|
||||
p99: null,
|
||||
};
|
||||
|
||||
function measuredIteration(
|
||||
workers: readonly WorkerCaptureMetrics[]
|
||||
): CancellationIterationResult {
|
||||
@@ -157,14 +168,28 @@ function measuredIteration(
|
||||
} as CancellationIterationResult['main'],
|
||||
phases: {} as CancellationIterationResult['phases'],
|
||||
renderer: {
|
||||
cpuProfilePath: null,
|
||||
frameGap: EMPTY_DISTRIBUTION,
|
||||
heapSnapshotPath: null,
|
||||
heartbeatDelay: EMPTY_DISTRIBUTION,
|
||||
longTask: EMPTY_DISTRIBUTION,
|
||||
peakHeapUsedBytes: 0,
|
||||
postGcHeapUsedBytes: null,
|
||||
probe: {
|
||||
cancelButtonFound: true,
|
||||
cancelClickEpochMs: null,
|
||||
events: [],
|
||||
frameGapsMs: [],
|
||||
heartbeatDelaysMs: [],
|
||||
longTasksMs: [],
|
||||
operationStartEpochMs: 0,
|
||||
terminalEpochMs: null,
|
||||
uiPaintedEpochMs: null,
|
||||
uiPhaseEpochMs: {},
|
||||
uiSettledEpochMs: null,
|
||||
},
|
||||
} as CancellationIterationResult['renderer'],
|
||||
tracePath: null,
|
||||
},
|
||||
runId: 'measured-1',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ export interface MainCaptureGenerationTransport {
|
||||
}[];
|
||||
}
|
||||
|
||||
// A parsed capture always carries its four work timestamps; only the
|
||||
// unavailable fallback reports them as null.
|
||||
type ParsedWorkerPerformanceCapture = Pick<
|
||||
WorkerRequestPerformanceMetrics,
|
||||
| 'eventLoopDelay'
|
||||
@@ -43,14 +45,15 @@ type ParsedWorkerPerformanceCapture = Pick<
|
||||
| 'histogramFlushedEpochMs'
|
||||
| 'invalidReason'
|
||||
| 'phaseEvents'
|
||||
| 'requestReceivedEpochMs'
|
||||
| 'responsePostedEpochMs'
|
||||
| 'threadCpuSystemMicros'
|
||||
| 'threadCpuUnavailableReason'
|
||||
| 'threadCpuUserMicros'
|
||||
| 'workEndedEpochMs'
|
||||
| 'workStartedEpochMs'
|
||||
>;
|
||||
> & {
|
||||
readonly requestReceivedEpochMs: number;
|
||||
readonly responsePostedEpochMs: number;
|
||||
readonly workEndedEpochMs: number;
|
||||
readonly workStartedEpochMs: number;
|
||||
};
|
||||
|
||||
const INVALID_REASONS = new Set(['overlapping-database-worker-requests']);
|
||||
const EVENT_LOOP_DELAY_REASONS = new Set([
|
||||
|
||||
@@ -16,11 +16,11 @@ const modulePromise = import(
|
||||
test('rejects a timed-out worker sample before returning plain capture metrics', async () => {
|
||||
const module = await modulePromise;
|
||||
const assertCaptureValid = module.assertWorkerSampleCaptureValid;
|
||||
assert.equal(typeof assertCaptureValid, 'function');
|
||||
assert.ok(typeof assertCaptureValid === 'function');
|
||||
|
||||
assert.doesNotThrow(() => assertCaptureValid?.([]));
|
||||
assert.doesNotThrow(() => assertCaptureValid([]));
|
||||
assert.throws(
|
||||
() => assertCaptureValid?.(['worker-sample-timeout']),
|
||||
() => assertCaptureValid(['worker-sample-timeout']),
|
||||
/main-capture-worker-sample-timeout/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ async function restoreSerializableApi(): Promise<WorkerSampleDeadlineApi> {
|
||||
const module = await modulePromise;
|
||||
assert.ok(module, 'worker sample deadline module must exist');
|
||||
const factory = module.createWorkerSampleDeadlineApi;
|
||||
assert.equal(typeof factory, 'function');
|
||||
assert.ok(typeof factory === 'function');
|
||||
|
||||
const source = factory.toString();
|
||||
assert.doesNotMatch(source, /__name/);
|
||||
@@ -127,7 +127,7 @@ test('ignores an already queued deadline callback after successful completion',
|
||||
const operation = deferred<number>();
|
||||
const sampleKey = {};
|
||||
const applied: number[] = [];
|
||||
let queuedTimeout: (() => void) | null = null;
|
||||
let queuedTimeout = null as (() => void) | null;
|
||||
let timeoutCount = 0;
|
||||
const timers: WorkerSampleDeadlineTimers = {
|
||||
clearTimeout(): void {
|
||||
|
||||
@@ -369,8 +369,14 @@ export async function runXtreamBenchmarkIteration(
|
||||
await prepared?.dispose().catch(() => undefined);
|
||||
await renderer?.dispose().catch(() => undefined);
|
||||
if (app && dataDirectory) {
|
||||
const launchedApp = app;
|
||||
const launchedDataDirectory = dataDirectory;
|
||||
await runXtreamFinalTeardown(
|
||||
() => disposeXtreamBenchmarkApp(app, dataDirectory),
|
||||
() =>
|
||||
disposeXtreamBenchmarkApp(
|
||||
launchedApp,
|
||||
launchedDataDirectory
|
||||
),
|
||||
propagatedFailure
|
||||
);
|
||||
}
|
||||
|
||||
@@ -321,16 +321,14 @@ export function isExactXtreamProcessIdentity(value: unknown): boolean {
|
||||
'startupAttemptCount',
|
||||
'startupRetryReasons',
|
||||
] as const;
|
||||
if (!isExactRecord(value, keys)) return false;
|
||||
const startupAttemptCount = isPositiveSafeInteger(
|
||||
isExactRecord(value, keys) ? value['startupAttemptCount'] : null
|
||||
value['startupAttemptCount']
|
||||
)
|
||||
? Number(value['startupAttemptCount'])
|
||||
: 0;
|
||||
const startupRetryReasons = isExactRecord(value, keys)
|
||||
? value['startupRetryReasons']
|
||||
: null;
|
||||
const startupRetryReasons = value['startupRetryReasons'];
|
||||
return (
|
||||
isExactRecord(value, keys) &&
|
||||
isPositiveSafeInteger(value['captureGeneration']) &&
|
||||
isPositiveSafeInteger(value['electronPid']) &&
|
||||
value['freshProcess'] === true &&
|
||||
|
||||
+40
-24
@@ -4,7 +4,10 @@ import { describe, it } from 'node:test';
|
||||
import { XTREAM_SCENARIO_ID } from './xtream-benchmark-contract';
|
||||
import { createXtreamAssemblerFixture } from './xtream-iteration-assembler.fixture';
|
||||
import { assembleXtreamRawIteration } from './xtream-iteration-assembler';
|
||||
import { summarizeXtreamUiActionSamples } from './xtream-ui-action-probe';
|
||||
import {
|
||||
summarizeXtreamUiActionSamples,
|
||||
type XtreamUiActionProbeResult,
|
||||
} from './xtream-ui-action-probe';
|
||||
|
||||
describe('Xtream raw iteration assembler background UI evidence', () => {
|
||||
it('binds samples to the full refresh lifecycle after the delete prelude', () => {
|
||||
@@ -23,9 +26,10 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
|
||||
assert.ok(background);
|
||||
assert.ok(operation);
|
||||
assert.ok(storeTerminalEpochMs);
|
||||
const deleteStartedEpochMs = request.workStartedEpochMs;
|
||||
const step = (storeTerminalEpochMs - request.workEndedEpochMs) / 12;
|
||||
assert.ok(step > 0);
|
||||
const samples = background.result.samples.map((sample, index) => {
|
||||
const samples = uiResult(background).samples.map((sample, index) => {
|
||||
const startEpochMs =
|
||||
Number(request.workEndedEpochMs) + step * (index * 2 + 1);
|
||||
const endEpochMs = startEpochMs + step;
|
||||
@@ -51,7 +55,7 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
|
||||
dashboardPaintVisibility: 'visible-without-overlay',
|
||||
samples,
|
||||
},
|
||||
startedEpochMs: request.workStartedEpochMs,
|
||||
startedEpochMs: deleteStartedEpochMs,
|
||||
},
|
||||
operation: {
|
||||
...operation,
|
||||
@@ -78,7 +82,7 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
|
||||
const step =
|
||||
(request.workEndedEpochMs - background.startedEpochMs) / 12;
|
||||
assert.ok(step > 0);
|
||||
const samples = background.result.samples.map((sample, index) => {
|
||||
const samples = uiResult(background).samples.map((sample, index) => {
|
||||
const startEpochMs =
|
||||
background.startedEpochMs + step * (index * 2 + 1);
|
||||
const endEpochMs = startEpochMs + step;
|
||||
@@ -123,17 +127,19 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
|
||||
);
|
||||
const background = input.terminal.backgroundProbe;
|
||||
assert.ok(background);
|
||||
const samples = background.result.samples.map((sample, index, all) => {
|
||||
if (index !== 1) return sample;
|
||||
const previous = all[0];
|
||||
assert.ok(previous);
|
||||
const startEpochMs = previous.endEpochMs - 0.1;
|
||||
return {
|
||||
...sample,
|
||||
latencyMs: sample.endEpochMs - startEpochMs,
|
||||
startEpochMs,
|
||||
};
|
||||
});
|
||||
const samples = uiResult(background).samples.map(
|
||||
(sample, index, all) => {
|
||||
if (index !== 1) return sample;
|
||||
const previous = all[0];
|
||||
assert.ok(previous);
|
||||
const startEpochMs = previous.endEpochMs - 0.1;
|
||||
return {
|
||||
...sample,
|
||||
latencyMs: sample.endEpochMs - startEpochMs,
|
||||
startEpochMs,
|
||||
};
|
||||
}
|
||||
);
|
||||
const summary = summarizeXtreamUiActionSamples(samples);
|
||||
|
||||
assert.throws(
|
||||
@@ -166,15 +172,17 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
|
||||
input.rendererCapture.probe.storeTerminalEpochMs;
|
||||
assert.ok(background);
|
||||
assert.ok(storeTerminalEpochMs);
|
||||
const samples = background.result.samples.map((sample, index, all) => {
|
||||
if (index !== all.length - 1) return sample;
|
||||
const endEpochMs = storeTerminalEpochMs + 0.1;
|
||||
return {
|
||||
...sample,
|
||||
endEpochMs,
|
||||
latencyMs: endEpochMs - sample.startEpochMs,
|
||||
};
|
||||
});
|
||||
const samples = uiResult(background).samples.map(
|
||||
(sample, index, all) => {
|
||||
if (index !== all.length - 1) return sample;
|
||||
const endEpochMs = storeTerminalEpochMs + 0.1;
|
||||
return {
|
||||
...sample,
|
||||
endEpochMs,
|
||||
latencyMs: endEpochMs - sample.startEpochMs,
|
||||
};
|
||||
}
|
||||
);
|
||||
const summary = summarizeXtreamUiActionSamples(samples);
|
||||
|
||||
assert.throws(
|
||||
@@ -199,3 +207,11 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// The background-UI fixture builds its probe result from the UI action probe;
|
||||
// the terminal evidence type erases it to unknown.
|
||||
function uiResult(background: {
|
||||
readonly result: unknown;
|
||||
}): XtreamUiActionProbeResult {
|
||||
return background.result as XtreamUiActionProbeResult;
|
||||
}
|
||||
@@ -336,6 +336,7 @@ describe('Xtream raw iteration assembler', () => {
|
||||
);
|
||||
const rendererTerminalEpochMs =
|
||||
input.rendererCapture.probe.uiPaintedEpochMs;
|
||||
assert.ok(rendererTerminalEpochMs !== null);
|
||||
|
||||
const result = assembleXtreamRawIteration({
|
||||
...input,
|
||||
|
||||
@@ -107,6 +107,7 @@ function backgroundMetrics(
|
||||
operation.startedEpochMs > background.startedEpochMs ||
|
||||
operation.terminalEpochMs > input.terminal.terminalEpochMs ||
|
||||
background.completedEpochMs > input.terminal.terminalEpochMs ||
|
||||
input.renderer.probe.uiPaintedEpochMs === null ||
|
||||
background.completedEpochMs >= input.renderer.probe.uiPaintedEpochMs
|
||||
) {
|
||||
invalid();
|
||||
|
||||
@@ -13,7 +13,8 @@ describe('Xtream legacy IPC clock skew', () => {
|
||||
const request = input.mainCapture.requests.find(
|
||||
({ operation }) => operation === 'DB_UPSERT_APP_PLAYLIST'
|
||||
);
|
||||
assert.equal(typeof request?.responseEpochMs, 'number');
|
||||
assert.ok(request);
|
||||
assert.equal(typeof request.responseEpochMs, 'number');
|
||||
const updateSuccessEpoch = (sourceEpochMs: number) => ({
|
||||
...input,
|
||||
mainCapture: {
|
||||
@@ -22,7 +23,7 @@ describe('Xtream legacy IPC clock skew', () => {
|
||||
...input.mainCapture.capture,
|
||||
timeline: input.mainCapture.capture.timeline.map((record) =>
|
||||
record.type === 'preload-performance-success' &&
|
||||
record.ipcCallId === request?.ipcCallId &&
|
||||
record.ipcCallId === request.ipcCallId &&
|
||||
record.operation === request.operation
|
||||
? { ...record, sourceEpochMs }
|
||||
: record
|
||||
@@ -32,7 +33,7 @@ describe('Xtream legacy IPC clock skew', () => {
|
||||
});
|
||||
|
||||
const normalized = assembleXtreamRawIteration(
|
||||
updateSuccessEpoch(Number(request?.responseEpochMs) - 0.25)
|
||||
updateSuccessEpoch(Number(request.responseEpochMs) - 0.25)
|
||||
);
|
||||
const response = normalized.phaseCapture.ipcSpans.find(
|
||||
({ boundary, method }) =>
|
||||
@@ -45,7 +46,7 @@ describe('Xtream legacy IPC clock skew', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
assembleXtreamRawIteration(
|
||||
updateSuccessEpoch(Number(request?.responseEpochMs) - 1)
|
||||
updateSuccessEpoch(Number(request.responseEpochMs) - 1)
|
||||
),
|
||||
/xtream-iteration-assembly-invalid/
|
||||
);
|
||||
|
||||
@@ -155,8 +155,9 @@ test('rejects incomplete and internally inconsistent terminal evidence', async (
|
||||
|
||||
test('keeps a completed delete inside the identified refresh lifecycle until store terminal', async () => {
|
||||
const module = await modulePromise;
|
||||
assert.ok(module?.assertXtreamBackgroundRefreshActive);
|
||||
assert.ok(module.XTREAM_RENDERER_STATE_KEY);
|
||||
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 = {
|
||||
@@ -171,17 +172,13 @@ test('keeps a completed delete inside the identified refresh lifecycle until sto
|
||||
};
|
||||
target[stateKey] = backgroundRefreshState(identity);
|
||||
try {
|
||||
await assert.doesNotReject(() =>
|
||||
module.assertXtreamBackgroundRefreshActive?.(page, identity)
|
||||
);
|
||||
await assert.doesNotReject(() => assertRefreshActive(page, identity));
|
||||
target[stateKey] = {
|
||||
...backgroundRefreshState(identity),
|
||||
storeTerminalEpochMs: 200,
|
||||
};
|
||||
await assert.rejects(
|
||||
() =>
|
||||
module.assertXtreamBackgroundRefreshActive?.(page, identity) ??
|
||||
Promise.resolve(),
|
||||
() => assertRefreshActive(page, identity),
|
||||
/xtream-background-refresh-not-active/
|
||||
);
|
||||
target[stateKey] = backgroundRefreshState({
|
||||
@@ -189,9 +186,7 @@ test('keeps a completed delete inside the identified refresh lifecycle until sto
|
||||
operationId: 'different-operation',
|
||||
});
|
||||
await assert.rejects(
|
||||
() =>
|
||||
module.assertXtreamBackgroundRefreshActive?.(page, identity) ??
|
||||
Promise.resolve(),
|
||||
() => assertRefreshActive(page, identity),
|
||||
/xtream-background-refresh-not-active/
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -97,7 +97,6 @@ export async function prepareXtreamScenario(
|
||||
options: PrepareXtreamScenarioOptions
|
||||
): Promise<PreparedXtreamScenario> {
|
||||
validateOptions(options);
|
||||
const cancellationObserver: XtreamCancellationObserver | null = null;
|
||||
let backgroundOperation: XtreamOperationWindow | null = null;
|
||||
try {
|
||||
if (options.scenarioId === XTREAM_SCENARIO_ID.BACKGROUND_UI) {
|
||||
@@ -106,16 +105,13 @@ export async function prepareXtreamScenario(
|
||||
const trigger = await prepareFinalTrigger(page, options);
|
||||
return createPreparedScenario({
|
||||
backgroundOperation,
|
||||
cancellationObserver,
|
||||
cancellationObserver: null,
|
||||
options,
|
||||
page,
|
||||
trigger,
|
||||
});
|
||||
} catch (error) {
|
||||
await Promise.allSettled([
|
||||
cancellationObserver?.dispose(),
|
||||
backgroundOperation?.dispose(),
|
||||
]);
|
||||
await Promise.allSettled([backgroundOperation?.dispose()]);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('Xtream terminal settlement ordering', () => {
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
assert.deepEqual(calls, []);
|
||||
assert.deepEqual<string[]>(calls, []);
|
||||
|
||||
rendererTerminal.resolve();
|
||||
await waitFor(() => calls.includes('stop-main'));
|
||||
|
||||
@@ -458,7 +458,9 @@ async function waitForRemoteStatus(
|
||||
port: number,
|
||||
predicate: (status: RemoteControlStatus) => boolean
|
||||
): Promise<RemoteControlStatus> {
|
||||
let latestStatus: RemoteControlStatus | null = null;
|
||||
// Typed through an assertion so the poll callback's writes stay visible
|
||||
// to the return below instead of narrowing to the initial null.
|
||||
let latestStatus = null as RemoteControlStatus | null;
|
||||
|
||||
try {
|
||||
await expect
|
||||
@@ -479,7 +481,10 @@ async function waitForRemoteStatus(
|
||||
);
|
||||
}
|
||||
|
||||
return latestStatus as RemoteControlStatus;
|
||||
if (!latestStatus) {
|
||||
throw new Error('Remote status poll passed without a status payload');
|
||||
}
|
||||
return latestStatus;
|
||||
}
|
||||
|
||||
async function getRemoteStatus(
|
||||
|
||||
@@ -361,7 +361,7 @@ test.describe('Electron Settings', () => {
|
||||
'app-html-video-player video'
|
||||
);
|
||||
await expect(video).toBeAttached();
|
||||
await video.evaluate<void, HTMLVideoElement>((video) => {
|
||||
await video.evaluate((video: HTMLVideoElement) => {
|
||||
const ownerDocument = video.ownerDocument;
|
||||
let activePictureInPictureElement: Element | null = null;
|
||||
video.dataset['pictureInPictureRequestCount'] = '0';
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"lib": ["es2022", "dom"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/performance/*journey*.ts", "src/journeys/**/*.ts"]
|
||||
"include": ["src/**/*.ts", "*.ts"]
|
||||
}
|
||||
@@ -1507,9 +1507,8 @@ detail 187 to 143 MB.
|
||||
`node:test` (`pnpm nx run electron-backend-e2e:test-performance-harness`).
|
||||
`tsx` runs those specs without type checking, so also run
|
||||
`pnpm run typecheck:spec electron-backend-e2e`. Its
|
||||
`apps/electron-backend-e2e/tsconfig.spec.json` includes `src/journeys/**`
|
||||
and `src/performance/*journey*.ts`; keep `journey` in the name of the
|
||||
journey's files and specs under `src/performance/`.
|
||||
`apps/electron-backend-e2e/tsconfig.spec.json` includes every TypeScript
|
||||
file in the project, so a new journey file needs no `include` change.
|
||||
6. Validate a counter before it becomes a guardrail: one PR must show that
|
||||
lowering it moved wall-clock in the same journey.
|
||||
|
||||
|
||||
@@ -63,10 +63,9 @@ whose types drifted from the code it exercises. CI runs it in the
|
||||
the harness's `import.meta`, BigInt literals and ES2022 library names. It
|
||||
also sets `types: ["node"]`: the specs import `describe` and `it` from
|
||||
`node:test`, so a Jest or Mocha global fails the check. Its `include` covers
|
||||
the journey harness (`src/performance/*journey*.ts` and `src/journeys/**`);
|
||||
the older benchmark files and the Electron E2E specs still have type errors,
|
||||
so widen the `include` as they are fixed instead of adding a baseline or an
|
||||
ignore list.
|
||||
the whole project (`src/**/*.ts` and the root Playwright configs): the
|
||||
journey harness, the benchmark harness and its specs, and the Electron E2E
|
||||
specs, with no baseline or ignore list.
|
||||
- Type test doubles instead of casting to `any`: `jest.Mocked<T>`,
|
||||
`InstanceType<typeof SomeStore>` for signal stores, and
|
||||
`Object.defineProperty` or a writable mapped type for read-only capability
|
||||
@@ -331,7 +330,7 @@ written to the same summary file; its probe specs run with
|
||||
`pnpm nx run electron-backend-e2e:test-performance-harness`, which CI runs in
|
||||
the `Unit Tests and Typechecks` job of `ci.yml` on every run. `tsx` runs them
|
||||
without type checking; `pnpm run typecheck:spec electron-backend-e2e` checks
|
||||
the journey harness (see Unit And Type Checks). The
|
||||
the whole project, those specs included (see Unit And Type Checks). The
|
||||
`electron-backend-e2e` command targets call `tsx` and `playwright` directly,
|
||||
not through `pnpm exec`: under `pnpm nx`, a nested `pnpm exec` can run from the
|
||||
workspace root instead of the target `cwd` and miss cwd-relative specs, globs
|
||||
|
||||
Reference in new issue
Block a user