Files
iptvnator/apps/electron-backend-e2e/src/performance/xtream-renderer-probe.ts
T
4grayandClaude Opus 5 9b7776a901 chore(lint): hold tests to their own max-lines ceiling (#1306)
* chore(lint): hold tests to their own max-lines ceiling

The flat 400-line cap treated a spec like a component. A spec is a flat
list of independent cases, so hitting the cap there produces arbitrary
`-2.spec.ts` splits and hides coverage instead of surfacing design debt —
65 of the 138 files over the limit were tests.

Production code keeps 400. Tests (`**/*.spec.ts`, `**/*.e2e.ts`, and
everything under `apps/*-e2e/**`) get 1200. Blank lines and comments no
longer count, so a docblock can't be the reason a file must be split.

Both limits now live in tools/eslint/max-lines-config.mjs, imported by
eslint.config.mjs and the baseline generator alike. The generator decides
who belongs on the list by running ESLint's own max-lines rule instead of
counting lines itself — a private reimplementation would disagree with the
rule the moment either side changed (a `//` inside a template literal is
enough) and yield a baseline that turns CI red while looking correct.

The baseline drops 126 -> 68 entries with nothing added, and six now-dead
`eslint-disable max-lines` directives are removed. A new eslint-tools test
asserts the committed baseline still matches what the generator produces,
so a stale entry or a forgotten regeneration fails CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(lint): classify eslint-tools in the coverage policy

A project with a `test` target must be assigned a coverage tier, so
adding eslint-tools broke `coverage:policy:check` before the unit suite
even ran. Tier B alongside packaging and release-tools: these are Node
tests over lint tooling, and a coverage percentage across a generated
list would not mean anything.

CI runs Tier B/C through its own `--run-non-tier-a` step, so the
baseline-consistency test executes there rather than being skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 08:08:04 +02:00

409 lines
17 KiB
TypeScript

import type { Page } from '@playwright/test';
import {
RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
type RendererPerformancePhaseEvent,
type RendererPerformancePhaseHook,
} from '@iptvnator/shared/logging';
import type { XtreamScenarioId } from './xtream-benchmark-contract';
import {
XTREAM_RENDERER_STATE_KEY,
XTREAM_RENDERER_STOPPED_KEY,
type XtreamRendererDbEvent,
type XtreamRendererProbeOptions,
} from './xtream-renderer-probe-contract';
export * from './xtream-renderer-probe-contract';
export async function installXtreamRendererProbe(
page: Page,
options: XtreamRendererProbeOptions
): Promise<void> {
await page.evaluate(
({ hookKey, scenarioId, sourceTitle, stateKey, stoppedKey }) => {
interface DbEvent {
current?: number;
increment?: number;
operation?: string;
operationId?: string;
phase?: string;
playlistId?: string;
status?: string;
total?: number;
}
interface State {
activeDeleteOperationId: string | null;
cancelEligibleProgressEpochMs: number | null;
cancellationClickEpochMs: number | null;
dbCancellationTerminalEpochMs: number | null;
dbOperationEvents: XtreamRendererDbEvent[];
dbUnsubscribe: (() => void) | null;
domReadyEpochMs: number | null;
frameGapsMs: number[];
frameRequestId: number;
heartbeatDelaysMs: number[];
invalidReason: string | null;
lastFrameAt: number | null;
longTaskObserver: PerformanceObserver | null;
longTasksMs: number[];
operationStartEpochMs: number;
operationStartMonotonicMs: number;
paintFrameCount: number;
performancePhaseEvents: RendererPerformancePhaseEvent[];
recordLongTask(entry: PerformanceEntry): void;
removeCancelClickListener: (() => void) | null;
routeReadyEpochMs: number | null;
saveContentOperationId: string | null;
scenarioId: XtreamScenarioId;
sourceTitle: string;
storeTerminalCount: number;
storeTerminalEpochMs: number | null;
terminalEpochMs: number | null;
uiPaintedEpochMs: number | null;
}
const target = globalThis as unknown as Record<
PropertyKey,
unknown
>;
const hookSymbol = Symbol.for(hookKey);
if (
target[stateKey] !== undefined ||
target[hookSymbol] !== undefined
) {
throw new Error('xtream-renderer-probe-already-installed');
}
delete target[stoppedKey];
const epoch = (): number =>
performance.timeOrigin + performance.now();
const numberOrNull = (value: unknown): number | null =>
typeof value === 'number' &&
Number.isFinite(value) &&
value >= 0
? value
: null;
const stringOrNull = (value: unknown): string | null =>
typeof value === 'string' && value.length > 0 ? value : null;
const state: State = {
activeDeleteOperationId: null,
cancelEligibleProgressEpochMs: null,
cancellationClickEpochMs: null,
dbCancellationTerminalEpochMs: null,
dbOperationEvents: [],
dbUnsubscribe: null,
domReadyEpochMs: null,
frameGapsMs: [],
frameRequestId: 0,
heartbeatDelaysMs: [],
invalidReason: null,
lastFrameAt: null,
longTaskObserver: null,
longTasksMs: [],
operationStartEpochMs: 0,
operationStartMonotonicMs: 0,
paintFrameCount: 0,
performancePhaseEvents: [],
recordLongTask: (entry) => {
const entryEpochMs =
performance.timeOrigin + entry.startTime;
const measuredStartEpochMs = Math.max(
entryEpochMs,
state.operationStartEpochMs
);
const measuredEndEpochMs = Math.min(
entryEpochMs + entry.duration,
state.terminalEpochMs ?? Number.POSITIVE_INFINITY
);
if (
state.operationStartEpochMs > 0 &&
measuredEndEpochMs > measuredStartEpochMs
) {
state.longTasksMs.push(
measuredEndEpochMs - measuredStartEpochMs
);
}
},
removeCancelClickListener: null,
routeReadyEpochMs: null,
saveContentOperationId: null,
scenarioId,
sourceTitle,
storeTerminalCount: 0,
storeTerminalEpochMs: null,
terminalEpochMs: null,
uiPaintedEpochMs: null,
};
const routeHydratesAfterImport =
scenarioId === 'xtream-refresh-large' ||
scenarioId === 'xtream-background-ui';
const requiredStoreTerminalCount = routeHydratesAfterImport ? 2 : 1;
const isVisible = (element: Element | null): boolean =>
element instanceof HTMLElement &&
element.getClientRects().length > 0;
const catalogVisible = (): boolean =>
isVisible(
document.querySelector(
'app-workspace-context-panel .category-item, ' +
'.content-card, [data-test-id="channel-item"], mat-card'
)
);
const overlayGone = (): boolean =>
!isVisible(
document.querySelector('.workspace-loading-overlay')
);
const sourceRowAbsent = (): boolean =>
!Array.from(
document.querySelectorAll('app-playlist-item')
).some(
(row) =>
row
.querySelector('.playlist-title')
?.textContent?.trim() === state.sourceTitle
);
const terminalDomGate = (): boolean => {
if (
state.storeTerminalEpochMs === null ||
!overlayGone() ||
state.invalidReason !== null
) {
return false;
}
if (scenarioId === 'xtream-cancel-import') {
return (
location.pathname.includes('/workspace/xtreams/') &&
state.dbCancellationTerminalEpochMs !== null &&
isVisible(
document.querySelector('.xtream-content-gate')
)
);
}
if (scenarioId === 'xtream-delete-large') {
return (
sourceRowAbsent() &&
!location.pathname.includes('/workspace/xtreams/')
);
}
return (
location.pathname.includes('/workspace/xtreams/') &&
catalogVisible()
);
};
const phaseHook: RendererPerformancePhaseHook = (event) => {
state.performancePhaseEvents.push(event);
const requiredPhase =
scenarioId === 'xtream-delete-large'
? 'store.xtream-delete-row'
: 'store.xtream-import-terminal';
if (
state.operationStartEpochMs > 0 &&
event.boundary === 'end' &&
event.outcome === 'success' &&
event.phase === requiredPhase
) {
state.storeTerminalCount += 1;
if (
state.storeTerminalCount === requiredStoreTerminalCount
) {
state.storeTerminalEpochMs = event.epochMs;
} else if (
state.storeTerminalCount > requiredStoreTerminalCount
) {
state.invalidReason ??= 'duplicate-store-terminal';
}
}
};
target[hookSymbol] = phaseHook;
const onDbEvent = (event: DbEvent): void => {
const sampledEpochMs = epoch();
const operation = stringOrNull(event.operation);
const status = stringOrNull(event.status);
if (operation === null || status === null) {
state.invalidReason ??= 'invalid-db-operation-event';
return;
}
const sanitized: XtreamRendererDbEvent = {
current: numberOrNull(event.current),
epochMs: sampledEpochMs,
increment: numberOrNull(event.increment),
operation,
operationId: stringOrNull(event.operationId),
phase: stringOrNull(event.phase),
playlistId: stringOrNull(event.playlistId),
status,
total: numberOrNull(event.total),
};
state.dbOperationEvents.push(sanitized);
if (operation === 'delete-xtream-content') {
if (status === 'started' || status === 'progress') {
state.activeDeleteOperationId ??= sanitized.operationId;
if (
state.activeDeleteOperationId === null ||
sanitized.operationId !==
state.activeDeleteOperationId
) {
state.invalidReason ??=
'ambiguous-delete-xtream-content';
}
} else if (
sanitized.operationId ===
state.activeDeleteOperationId &&
['cancelled', 'completed', 'error'].includes(status)
) {
state.activeDeleteOperationId = null;
}
}
if (
scenarioId !== 'xtream-cancel-import' ||
operation !== 'save-content' ||
state.operationStartEpochMs <= 0
) {
return;
}
if (status === 'started' || status === 'progress') {
state.saveContentOperationId ??= sanitized.operationId;
if (
state.saveContentOperationId === null ||
sanitized.operationId !== state.saveContentOperationId
) {
state.invalidReason ??= 'ambiguous-save-content';
return;
}
}
if (
state.cancellationClickEpochMs !== null &&
(status === 'completed' || status === 'started')
) {
state.invalidReason ??= 'save-content-after-cancel';
return;
}
if (status === 'cancelled') {
if (
sanitized.operationId !== state.saveContentOperationId
) {
state.invalidReason ??= 'ambiguous-save-content';
return;
}
state.dbCancellationTerminalEpochMs = sampledEpochMs;
return;
}
const total = sanitized.total;
const current = sanitized.current;
if (
status !== 'progress' ||
total === null ||
current === null ||
current < Math.max(100, Math.floor(total * 0.1)) ||
state.cancelEligibleProgressEpochMs !== null
) {
return;
}
state.cancelEligibleProgressEpochMs = sampledEpochMs;
};
const onCancelClick = (event: Event): void => {
const clicked = event.target;
if (
scenarioId !== 'xtream-cancel-import' ||
!(clicked instanceof Element) ||
clicked.closest('.workspace-loading-overlay__action') ===
null
) {
return;
}
if (
state.operationStartEpochMs <= 0 ||
state.cancelEligibleProgressEpochMs === null
) {
state.invalidReason ??= 'cancel-click-before-progress';
return;
}
if (state.cancellationClickEpochMs !== null) {
state.invalidReason ??= 'duplicate-cancel-click';
return;
}
state.cancellationClickEpochMs = epoch();
};
document.addEventListener('click', onCancelClick, true);
state.removeCancelClickListener = () =>
document.removeEventListener('click', onCancelClick, true);
const bridge = (
globalThis as unknown as {
electron?: {
onDbOperationEvent?: (
listener: (event: DbEvent) => void
) => () => void;
};
}
).electron;
if (typeof bridge?.onDbOperationEvent !== 'function') {
throw new Error('xtream-db-operation-listener-unavailable');
}
state.dbUnsubscribe = bridge.onDbOperationEvent(onDbEvent);
const frame = (timestamp: number): void => {
if (
state.lastFrameAt !== null &&
state.operationStartEpochMs > 0 &&
state.terminalEpochMs === null
) {
const start = Math.max(
state.lastFrameAt,
state.operationStartMonotonicMs
);
if (timestamp > start)
state.frameGapsMs.push(timestamp - start);
}
state.lastFrameAt = timestamp;
const routeReady =
scenarioId === 'xtream-delete-large'
? location.pathname.endsWith('/workspace/sources')
: location.pathname.includes('/workspace/xtreams/');
if (state.operationStartEpochMs > 0 && routeReady) {
state.routeReadyEpochMs ??= epoch();
}
if (state.operationStartEpochMs > 0 && terminalDomGate()) {
state.domReadyEpochMs ??= epoch();
state.paintFrameCount += 1;
if (state.paintFrameCount >= 2) {
state.uiPaintedEpochMs = epoch();
state.terminalEpochMs = state.uiPaintedEpochMs;
}
} else {
state.paintFrameCount = 0;
}
state.frameRequestId =
state.terminalEpochMs === null
? requestAnimationFrame(frame)
: 0;
};
state.frameRequestId = requestAnimationFrame(frame);
if (
typeof PerformanceObserver === 'function' &&
PerformanceObserver.supportedEntryTypes.includes('longtask')
) {
state.longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
state.recordLongTask(entry);
}
});
state.longTaskObserver.observe({ entryTypes: ['longtask'] });
}
target[stateKey] = state;
},
{
hookKey: RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
scenarioId: options.scenarioId,
sourceTitle: options.sourceTitle,
stateKey: XTREAM_RENDERER_STATE_KEY,
stoppedKey: XTREAM_RENDERER_STOPPED_KEY,
}
);
}
export {
assertCompleteXtreamRendererProbe,
assertXtreamBackgroundDeleteActive,
assertXtreamBackgroundRefreshActive,
beginXtreamRendererOperation,
recordXtreamRendererHeartbeat,
stopXtreamRendererProbe,
waitForXtreamRendererTerminal,
} from './xtream-renderer-probe-control';