Skip to content

Commit 43629c0

Browse files
authored
refactor(dashboard): turn emitAnnotate into a request/response API (#40907)
1 parent 83ef991 commit 43629c0

3 files changed

Lines changed: 63 additions & 53 deletions

File tree

packages/playwright-core/src/tools/backend/devtools.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { libPath } from '../../package';
2222
import { defineTabTool, defineTool } from './tool';
2323
import { elementSchema, optionalElementSchema } from './snapshot';
2424

25-
import type { SubmittedAnnotationFrame } from '@dashboard/dashboardChannel';
25+
import type { AnnotateResult } from '../dashboard/dashboardController';
2626

2727
const resume = defineTool({
2828
capability: 'devtools',
@@ -157,11 +157,12 @@ const annotate = defineTabTool({
157157
response.addTextResult('No annotations were submitted.');
158158
return;
159159
}
160-
const { frames, feedback } = JSON.parse(text) as { frames: SubmittedAnnotationFrame[]; feedback: string };
161-
if (!frames || frames.length === 0) {
160+
const result = JSON.parse(text) as AnnotateResult;
161+
if (result.type !== 'submitted' || result.frames.length === 0) {
162162
response.addTextResult('No annotations were submitted.');
163163
return;
164164
}
165+
const { frames, feedback } = result;
165166
const date = new Date();
166167
if (feedback)
167168
response.addTextResult(feedback);

packages/playwright-core/src/tools/dashboard/dashboardApp.ts

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { RegistrySessionProvider } from './registrySessionProvider';
3232
import { IdentitySessionProvider } from './identitySessionProvider';
3333

3434
import type * as api from '../../..';
35-
import type { SubmittedAnnotationFrame } from '@dashboard/dashboardChannel';
35+
import type { AnnotateResult } from './dashboardController';
3636
import type { SessionProvider } from './sessionProvider';
3737

3838
// HMR: build-time flag — `true` in watch builds, `false` in release. esbuild
@@ -43,7 +43,7 @@ declare const __PW_HMR__: boolean;
4343
type DashboardServer = {
4444
url: string;
4545
reveal: (options: DashboardOptions) => Promise<void>;
46-
triggerAnnotate: (socket: net.Socket) => Promise<void>;
46+
triggerAnnotate: (signal: AbortSignal) => Promise<AnnotateResult>;
4747
close: () => Promise<void>;
4848
};
4949

@@ -53,18 +53,6 @@ async function startDashboardServer(provider: SessionProvider, options: Dashboar
5353

5454
const connections = new Set<DashboardConnection>();
5555
let connectionLanded = new ManualPromise<void>();
56-
const waitingSockets = new Set<net.Socket>();
57-
58-
const submitAnnotation = (frames: SubmittedAnnotationFrame[], feedback: string) => {
59-
if (waitingSockets.size === 0)
60-
return;
61-
const payload = JSON.stringify({ frames, feedback });
62-
for (const socket of waitingSockets) {
63-
socket.write(payload);
64-
socket.end();
65-
}
66-
waitingSockets.clear();
67-
};
6856

6957
httpServer.createWebSocket(() => {
7058
let connection: DashboardConnection;
@@ -75,7 +63,7 @@ async function startDashboardServer(provider: SessionProvider, options: Dashboar
7563
connectionLanded = new ManualPromise<void>();
7664
}, () => {
7765
connectionLanded.resolve();
78-
}, submitAnnotation);
66+
});
7967
connections.add(connection);
8068
return connection;
8169
});
@@ -108,24 +96,15 @@ async function startDashboardServer(provider: SessionProvider, options: Dashboar
10896
}));
10997
};
11098

111-
const triggerAnnotate = async (socket: net.Socket) => {
112-
waitingSockets.add(socket);
113-
const cleanup = () => {
114-
if (!waitingSockets.delete(socket))
115-
return;
116-
if (waitingSockets.size === 0) {
117-
for (const connection of connections)
118-
connection.emitCancelAnnotate();
119-
}
120-
};
121-
socket.on('close', cleanup);
122-
socket.on('error', cleanup);
123-
99+
const triggerAnnotate = async (cancellation: AbortSignal): Promise<AnnotateResult> => {
124100
await connectionLanded;
125-
if (waitingSockets.size === 0)
126-
return;
127-
for (const connection of connections)
128-
connection.emitAnnotate();
101+
if (cancellation.aborted || connections.size === 0)
102+
return { type: 'cancelled' };
103+
// Multiple dashboard connections is theoretical today (one UI per daemon), server mode does not support annotate.
104+
// If two ever land, the first to submit wins but the losers stay in
105+
// annotation mode until their UI reloads — revisit if that becomes a real
106+
// scenario.
107+
return await Promise.race([...connections].map(c => c.emitAnnotate({ signal: cancellation })));
129108
};
130109

131110
const close = () => httpServer.stop();
@@ -364,10 +343,14 @@ async function startApp(server: net.Server, options: DashboardOptions) {
364343
}
365344
const { page, server: dashboard } = await statePromise;
366345
if (parsed.annotate) {
346+
const cancellation = new AbortController();
347+
socket.on('close', () => cancellation.abort());
348+
socket.on('error', () => cancellation.abort());
367349
try {
368350
await page?.bringToFront();
369351
await dashboard.reveal(parsed);
370-
await dashboard.triggerAnnotate(socket);
352+
const result = await dashboard.triggerAnnotate(cancellation.signal);
353+
socket.end(JSON.stringify(result));
371354
} catch (e) {
372355
socket.end(e);
373356
}

packages/playwright-core/src/tools/dashboard/dashboardController.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ import type { SubmittedAnnotationFrame, Tab } from '@dashboard/dashboardChannel'
3232
import type { BrowserDescriptor } from '../../serverRegistry';
3333
import type { SessionProvider } from './sessionProvider';
3434

35+
export type AnnotateResult =
36+
| { type: 'submitted', frames: SubmittedAnnotationFrame[], feedback: string }
37+
| { type: 'cancelled' };
38+
3539
export class DashboardConnection implements Transport {
3640
sendEvent?: (method: string, params: any) => void;
3741
close?: () => void;
@@ -40,20 +44,18 @@ export class DashboardConnection implements Transport {
4044
private _attachedPage: AttachedPage | undefined;
4145
private _onclose: () => void;
4246
private _onconnected?: () => void;
43-
private _onAnnotationSubmit?: (frames: SubmittedAnnotationFrame[], feedback: string) => void;
4447
private _pushTabsScheduled = false;
4548
private _visible = true;
4649
private _pendingReveal: { sessionName?: string; workspaceDir?: string; pageId?: string; done: ManualPromise<void> } | undefined;
47-
private _annotateWaitingForAttach = false;
50+
private _pendingAnnotate: { resolve: (result: AnnotateResult) => void; dispose: () => void } | undefined;
4851

4952
_recordingDir: string;
5053
_streams = new Map<string, { handle: fs.promises.FileHandle; path: string }>();
5154

52-
constructor(provider: SessionProvider, onclose: () => void, onconnected?: () => void, onAnnotationSubmit?: (frames: SubmittedAnnotationFrame[], feedback: string) => void) {
55+
constructor(provider: SessionProvider, onclose: () => void, onconnected?: () => void) {
5356
this._provider = provider;
5457
this._onclose = onclose;
5558
this._onconnected = onconnected;
56-
this._onAnnotationSubmit = onAnnotationSubmit;
5759
this._recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'playwright-recordings-'));
5860
}
5961

@@ -84,6 +86,7 @@ export class DashboardConnection implements Transport {
8486
// Reject any in-flight reveal so callers awaiting it don't hang.
8587
this._pendingReveal?.done.reject(new Error('Dashboard connection closed'));
8688
this._pendingReveal = undefined;
89+
this._resolvePendingAnnotate({ type: 'cancelled' });
8790
for (const stream of this._streams.values()) {
8891
void stream.handle.close()
8992
.catch(() => {})
@@ -193,11 +196,11 @@ export class DashboardConnection implements Transport {
193196
}
194197

195198
async submitAnnotation(params: { frames: SubmittedAnnotationFrame[]; feedback: string }) {
196-
this._onAnnotationSubmit?.(params.frames, params.feedback);
199+
this._resolvePendingAnnotate({ type: 'submitted', frames: params.frames, feedback: params.feedback });
197200
}
198201

199202
async cancelAnnotation() {
200-
this._onAnnotationSubmit?.([], '');
203+
this._resolvePendingAnnotate({ type: 'cancelled' });
201204
}
202205

203206
async reveal(params: { path: string }) {
@@ -245,18 +248,43 @@ export class DashboardConnection implements Transport {
245248
this.sendEvent?.('frame', { data, viewportWidth, viewportHeight });
246249
}
247250

248-
emitAnnotate() {
251+
emitAnnotate({ signal }: { signal: AbortSignal }): Promise<AnnotateResult> {
252+
return new Promise<AnnotateResult>(resolve => {
253+
if (signal.aborted) {
254+
resolve({ type: 'cancelled' });
255+
return;
256+
}
257+
// Latest emitAnnotate supersedes any in-flight one on the same connection.
258+
this._resolvePendingAnnotate({ type: 'cancelled' });
259+
const onAbort = () => {
260+
if (this._pendingAnnotate !== pending)
261+
return;
262+
this._pendingAnnotate = undefined;
263+
pending.dispose();
264+
this.sendEvent?.('cancelAnnotate', {});
265+
resolve({ type: 'cancelled' });
266+
};
267+
const pending: NonNullable<typeof this._pendingAnnotate> = {
268+
resolve,
269+
dispose: () => signal.removeEventListener('abort', onAbort),
270+
};
271+
this._pendingAnnotate = pending;
272+
signal.addEventListener('abort', onAbort);
273+
this._tryFireAnnotate();
274+
});
275+
}
276+
277+
private _tryFireAnnotate() {
249278
// Defer until a page is attached so the client can fetch a screenshot.
250-
if (!this._attachedPage) {
251-
this._annotateWaitingForAttach = true;
279+
if (!this._pendingAnnotate || !this._attachedPage)
252280
return;
253-
}
254281
this.sendEvent?.('annotate', {});
255282
}
256283

257-
emitCancelAnnotate() {
258-
this._annotateWaitingForAttach = false;
259-
this.sendEvent?.('cancelAnnotate', {});
284+
private _resolvePendingAnnotate(result: AnnotateResult) {
285+
this._pendingAnnotate?.resolve(result);
286+
this._pendingAnnotate?.dispose();
287+
this._pendingAnnotate = undefined;
260288
}
261289

262290
artifactsDirFor(context: api.BrowserContext): string {
@@ -326,10 +354,8 @@ export class DashboardConnection implements Transport {
326354
attached.dispose();
327355
throw e;
328356
}
329-
if (this._annotateWaitingForAttach && this._attachedPage === attached) {
330-
this._annotateWaitingForAttach = false;
331-
this.sendEvent?.('annotate', {});
332-
}
357+
if (this._attachedPage === attached)
358+
this._tryFireAnnotate();
333359
}
334360

335361
_handleAttachedPageClose(context: api.BrowserContext) {

0 commit comments

Comments
 (0)