From ee7471ea5bfc06d6468cac09f04160c52ca63595 Mon Sep 17 00:00:00 2001 From: erKam Date: Tue, 13 Oct 2026 18:33:35 +0000 Subject: [PATCH 1/2] test(overlay): wait for the tick and write hook in the real app diagnosis tests The tests slept 20 to 60 ms and hoped Angular had run its tick and the signal write hook had loaded by then. On a busy machine neither was true, so the timer test saw no cycle at all and the click test lost the signal it wrote. Wait for the app to be stable after each write, and poll until the write hook is set, instead of fixed delays. --- .../src/__tests__/cd-diagnosis-real.test.ts | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/devtools/src/__tests__/cd-diagnosis-real.test.ts b/packages/devtools/src/__tests__/cd-diagnosis-real.test.ts index 827be14a..17553b0d 100644 --- a/packages/devtools/src/__tests__/cd-diagnosis-real.test.ts +++ b/packages/devtools/src/__tests__/cd-diagnosis-real.test.ts @@ -2,12 +2,12 @@ import '@angular/compiler'; import * as core from '@angular/core'; import { Component } from '@angular/core'; -import { afterEach, describe, expect, it } from 'vitest'; +import { setPostSignalSetFn } from '@angular/core/primitives/signals'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { attachChangeDetection, type CdReport } from '../cd-overlay.ts'; import { findCdScheduler } from '../cd-diagnosis.ts'; import { angularAtLeast, bootstrapRoot, STANDALONE, type BootedApp } from './angular-version.ts'; -const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const ngOf = () => (globalThis as { ng?: unknown }).ng; const supported = angularAtLeast('20.0'); const signal = (value: T, debugName: string) => @@ -78,6 +78,13 @@ Component({ `, })(Shell); +/** Whether a signal write hook is set, such as the one a recording adds once its import resolves. */ +function hasSignalWriteHook(): boolean { + const hook = setPostSignalSetFn(null); + setPostSignalSetFn(hook); + return hook !== null; +} + function fakeRpc() { const handlers = new Map unknown>(); const reports: CdReport[] = []; @@ -110,13 +117,13 @@ describe.skipIf(!supported)('change detection diagnosis with a real app', () => document.body.innerHTML = ''; app = await bootstrapRoot(Shell); app.appRef.tick(); - await wait(20); + await app.appRef.whenStable(); const { my, handlers, reports } = fakeRpc(); const cd = attachChangeDetection(my, 'p1', ngOf, 50); stop = cd.stop; handlers.get('change-detection-record')!({ pageId: 'p1', on: true }); // The signal write hook loads with a dynamic import. - await wait(20); + await vi.waitFor(() => expect(hasSignalWriteHook()).toBe(true), { timeout: 3000 }); return { async report() { await cd.push(true); @@ -135,7 +142,7 @@ describe.skipIf(!supported)('change detection diagnosis with a real app', () => it('credits a click to its event, target and listener component, with the signal it wrote', async () => { const recording = await record(); (document.querySelector('app-counter button') as HTMLButtonElement).click(); - await wait(40); + await app!.appRef.whenStable(); const report = await recording.report(); const cycle = report.cycles.find((c) => c.cause?.kind === 'event'); expect(cycle?.cause).toMatchObject({ @@ -154,8 +161,14 @@ describe.skipIf(!supported)('change detection diagnosis with a real app', () => const counter = (ngOf() as { getComponent(el: Element): Counter }).getComponent( document.querySelector('app-counter')!, ); - setTimeout(() => counter.bump(), 0); - await wait(60); + await new Promise((resolve) => + setTimeout(() => { + counter.bump(); + resolve(); + }, 0), + ); + // The write schedules a tick, which Angular tracks as a pending task. + await app!.appRef.whenStable(); const report = await recording.report(); const cause = report.cycles.find((c) => c.cause?.signal === 'count')?.cause; expect(cause?.kind).toBe('signal'); @@ -168,7 +181,7 @@ describe.skipIf(!supported)('change detection diagnosis with a real app', () => const recording = await record(); for (let i = 0; i < 3; i++) { (document.getElementById('add') as HTMLButtonElement).click(); - await wait(40); + await app!.appRef.whenStable(); } const report = await recording.report(); const diagnosis = report.diagnosis!; From a38e48dbd13ab30a69bf8cee276ab4aa6b91b7d5 Mon Sep 17 00:00:00 2001 From: erKam Date: Tue, 13 Oct 2026 18:33:36 +0000 Subject: [PATCH 2/2] test(overlay): load the overlay outside the page highlight test timeout The highlight-in-page test imported overlay.ts inside its body, which spent 2 to 3 seconds transforming the module graph against the 5 second timeout, and faked requestAnimationFrame, so advancing a minute replayed the box's follow loop thousands of times. Import the overlay with the file and fake only setTimeout and clearTimeout, the timers under test. --- packages/devtools/src/__tests__/page-highlight.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/devtools/src/__tests__/page-highlight.test.ts b/packages/devtools/src/__tests__/page-highlight.test.ts index 7903bae9..3bf79f9c 100644 --- a/packages/devtools/src/__tests__/page-highlight.test.ts +++ b/packages/devtools/src/__tests__/page-highlight.test.ts @@ -1,5 +1,8 @@ // @vitest-environment jsdom import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +// Loaded with the file, not inside a test: its module graph takes seconds to +// transform, which would count against that test's timeout. +import { initOverlay } from '../overlay.ts'; import { clearHighlight, HIGHLIGHT_SAFETY_MS, @@ -118,9 +121,10 @@ describe('page highlight', () => { it('keeps a panel highlight until a clear arrives, and an agent highlight clears itself', async () => { vi.stubGlobal('BroadcastChannel', undefined); - const { initOverlay } = await import('../overlay.ts'); const stop = await initOverlay(); - vi.useFakeTimers(); + // Only the clear timers are under test. A faked requestAnimationFrame would + // replay the box's follow loop every 16 ms over the minute advanced below. + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }); try { document.body.innerHTML = ''; place(document.querySelector('app-card')!, { x: 1, y: 2, width: 3, height: 4 }); @@ -148,7 +152,6 @@ describe('page highlight', () => { it('accepts an SVG element from the highlight-in-page request', async () => { vi.stubGlobal('BroadcastChannel', undefined); - const { initOverlay } = await import('../overlay.ts'); const stop = await initOverlay(); try { document.body.innerHTML = '';