playwriter 0.0.33 → 0.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aria-snapshot.d.ts +68 -0
- package/dist/aria-snapshot.d.ts.map +1 -0
- package/dist/aria-snapshot.js +359 -0
- package/dist/aria-snapshot.js.map +1 -0
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +95 -5
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cdp-session.d.ts +24 -3
- package/dist/cdp-session.d.ts.map +1 -1
- package/dist/cdp-session.js +23 -0
- package/dist/cdp-session.js.map +1 -1
- package/dist/debugger-api.md +4 -3
- package/dist/debugger.d.ts +4 -3
- package/dist/debugger.d.ts.map +1 -1
- package/dist/debugger.js +3 -1
- package/dist/debugger.js.map +1 -1
- package/dist/editor-api.md +2 -2
- package/dist/editor.d.ts +2 -2
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +1 -0
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +151 -14
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.js +340 -5
- package/dist/mcp.test.js.map +1 -1
- package/dist/protocol.d.ts +12 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/react-source.d.ts +3 -3
- package/dist/react-source.d.ts.map +1 -1
- package/dist/react-source.js +3 -1
- package/dist/react-source.js.map +1 -1
- package/dist/scoped-fs.d.ts +94 -0
- package/dist/scoped-fs.d.ts.map +1 -0
- package/dist/scoped-fs.js +356 -0
- package/dist/scoped-fs.js.map +1 -0
- package/dist/styles-api.md +3 -3
- package/dist/styles.d.ts +3 -3
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +3 -1
- package/dist/styles.js.map +1 -1
- package/package.json +13 -13
- package/src/aria-snapshot.ts +446 -0
- package/src/assets/aria-labels-github-snapshot.txt +605 -0
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-google-snapshot.txt +110 -0
- package/src/assets/aria-labels-google.png +0 -0
- package/src/assets/aria-labels-hacker-news-snapshot.txt +1023 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/cdp-relay.ts +103 -5
- package/src/cdp-session.ts +50 -3
- package/src/debugger.ts +6 -4
- package/src/editor.ts +4 -3
- package/src/index.ts +8 -0
- package/src/mcp.test.ts +424 -5
- package/src/mcp.ts +242 -66
- package/src/prompt.md +209 -167
- package/src/protocol.ts +14 -1
- package/src/react-source.ts +5 -3
- package/src/scoped-fs.ts +411 -0
- package/src/styles.ts +5 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Page, Locator, ElementHandle } from 'playwright-core';
|
|
2
|
+
export interface AriaRef {
|
|
3
|
+
role: string;
|
|
4
|
+
name: string;
|
|
5
|
+
ref: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AriaSnapshotResult {
|
|
8
|
+
snapshot: string;
|
|
9
|
+
refToElement: Map<string, {
|
|
10
|
+
role: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}>;
|
|
13
|
+
refHandles: Array<{
|
|
14
|
+
ref: string;
|
|
15
|
+
handle: ElementHandle;
|
|
16
|
+
}>;
|
|
17
|
+
getRefsForLocators: (locators: Array<Locator | ElementHandle>) => Promise<Array<AriaRef | null>>;
|
|
18
|
+
getRefForLocator: (locator: Locator | ElementHandle) => Promise<AriaRef | null>;
|
|
19
|
+
getRefStringForLocator: (locator: Locator | ElementHandle) => Promise<string | null>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get an accessibility snapshot with utilities to look up aria refs for elements.
|
|
23
|
+
* Uses Playwright's internal aria-ref selector engine.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const { snapshot, getRefsForLocators } = await getAriaSnapshot({ page })
|
|
28
|
+
* const refs = await getRefsForLocators([page.locator('button'), page.locator('a')])
|
|
29
|
+
* // refs[0].ref is e.g. "e5" - use page.locator('aria-ref=e5') to select
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function getAriaSnapshot({ page }: {
|
|
33
|
+
page: Page;
|
|
34
|
+
}): Promise<AriaSnapshotResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Show Vimium-style labels on interactive elements.
|
|
37
|
+
* Labels are yellow badges positioned above each element showing the aria ref (e.g., "e1", "e2").
|
|
38
|
+
* Use with screenshots so agents can see which elements are interactive.
|
|
39
|
+
*
|
|
40
|
+
* Labels auto-hide after 5 seconds to prevent stale labels remaining on the page.
|
|
41
|
+
* Call this function again if the page HTML changes to get fresh labels.
|
|
42
|
+
*
|
|
43
|
+
* By default, only shows labels for truly interactive roles (button, link, textbox, etc.)
|
|
44
|
+
* to reduce visual clutter. Set `interactiveOnly: false` to show all elements with refs.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const { snapshot, labelCount } = await showAriaRefLabels({ page })
|
|
49
|
+
* await page.screenshot({ path: '/tmp/screenshot.png' })
|
|
50
|
+
* // Agent sees [e5] label on "Submit" button
|
|
51
|
+
* await page.locator('aria-ref=e5').click()
|
|
52
|
+
* // Labels auto-hide after 5 seconds, or call hideAriaRefLabels() manually
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function showAriaRefLabels({ page, interactiveOnly }: {
|
|
56
|
+
page: Page;
|
|
57
|
+
interactiveOnly?: boolean;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
snapshot: string;
|
|
60
|
+
labelCount: number;
|
|
61
|
+
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Remove all aria ref labels from the page.
|
|
64
|
+
*/
|
|
65
|
+
export declare function hideAriaRefLabels({ page }: {
|
|
66
|
+
page: Page;
|
|
67
|
+
}): Promise<void>;
|
|
68
|
+
//# sourceMappingURL=aria-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aria-snapshot.d.ts","sourceRoot":"","sources":["../src/aria-snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEnE,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACzD,UAAU,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,aAAa,CAAA;KAAE,CAAC,CAAA;IACzD,kBAAkB,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,GAAG,aAAa,CAAC,KAAK,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAA;IAChG,gBAAgB,EAAE,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;IAC/E,sBAAsB,EAAE,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACrF;AA8ED;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAwF3F;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,iBAAiB,CAAC,EAAE,IAAI,EAAE,eAAsB,EAAE,EAAE;IACxE,IAAI,EAAE,IAAI,CAAA;IACV,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B,GAAG,OAAO,CAAC;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAC,CA8MD;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAc/E"}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
const LABELS_CONTAINER_ID = '__playwriter_labels__';
|
|
2
|
+
// Roles that represent truly interactive elements (can be clicked, typed into, etc.)
|
|
3
|
+
const INTERACTIVE_ROLES = new Set([
|
|
4
|
+
'button',
|
|
5
|
+
'link',
|
|
6
|
+
'textbox',
|
|
7
|
+
'combobox',
|
|
8
|
+
'searchbox',
|
|
9
|
+
'checkbox',
|
|
10
|
+
'radio',
|
|
11
|
+
'slider',
|
|
12
|
+
'spinbutton',
|
|
13
|
+
'switch',
|
|
14
|
+
'menuitem',
|
|
15
|
+
'menuitemcheckbox',
|
|
16
|
+
'menuitemradio',
|
|
17
|
+
'option',
|
|
18
|
+
'tab',
|
|
19
|
+
'treeitem',
|
|
20
|
+
]);
|
|
21
|
+
// Color categories for different role types - warm color scheme
|
|
22
|
+
// Format: [gradient-top, gradient-bottom, border]
|
|
23
|
+
const ROLE_COLORS = {
|
|
24
|
+
// Links - yellow (Vimium-style)
|
|
25
|
+
link: ['#FFF785', '#FFC542', '#E3BE23'],
|
|
26
|
+
// Buttons - orange
|
|
27
|
+
button: ['#FFE0B2', '#FFCC80', '#FFB74D'],
|
|
28
|
+
// Text inputs - coral/red
|
|
29
|
+
textbox: ['#FFCDD2', '#EF9A9A', '#E57373'],
|
|
30
|
+
combobox: ['#FFCDD2', '#EF9A9A', '#E57373'],
|
|
31
|
+
searchbox: ['#FFCDD2', '#EF9A9A', '#E57373'],
|
|
32
|
+
spinbutton: ['#FFCDD2', '#EF9A9A', '#E57373'],
|
|
33
|
+
// Checkboxes/Radios/Switches - warm pink
|
|
34
|
+
checkbox: ['#F8BBD0', '#F48FB1', '#EC407A'],
|
|
35
|
+
radio: ['#F8BBD0', '#F48FB1', '#EC407A'],
|
|
36
|
+
switch: ['#F8BBD0', '#F48FB1', '#EC407A'],
|
|
37
|
+
// Sliders - peach
|
|
38
|
+
slider: ['#FFCCBC', '#FFAB91', '#FF8A65'],
|
|
39
|
+
// Menu items - salmon
|
|
40
|
+
menuitem: ['#FFAB91', '#FF8A65', '#FF7043'],
|
|
41
|
+
menuitemcheckbox: ['#FFAB91', '#FF8A65', '#FF7043'],
|
|
42
|
+
menuitemradio: ['#FFAB91', '#FF8A65', '#FF7043'],
|
|
43
|
+
// Tabs/Options - amber
|
|
44
|
+
tab: ['#FFE082', '#FFD54F', '#FFC107'],
|
|
45
|
+
option: ['#FFE082', '#FFD54F', '#FFC107'],
|
|
46
|
+
treeitem: ['#FFE082', '#FFD54F', '#FFC107'],
|
|
47
|
+
};
|
|
48
|
+
// Default yellow for unknown roles
|
|
49
|
+
const DEFAULT_COLORS = ['#FFF785', '#FFC542', '#E3BE23'];
|
|
50
|
+
// Use String.raw for CSS syntax highlighting in editors
|
|
51
|
+
const css = String.raw;
|
|
52
|
+
const LABEL_STYLES = css `
|
|
53
|
+
.__pw_label__ {
|
|
54
|
+
position: absolute;
|
|
55
|
+
font: bold 11px Helvetica, Arial, sans-serif;
|
|
56
|
+
padding: 1px 4px;
|
|
57
|
+
border-radius: 3px;
|
|
58
|
+
color: black;
|
|
59
|
+
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
|
|
60
|
+
white-space: nowrap;
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
const CONTAINER_STYLES = css `
|
|
64
|
+
position: absolute;
|
|
65
|
+
left: 0;
|
|
66
|
+
top: 0;
|
|
67
|
+
z-index: 2147483647;
|
|
68
|
+
pointer-events: none;
|
|
69
|
+
`;
|
|
70
|
+
/**
|
|
71
|
+
* Get an accessibility snapshot with utilities to look up aria refs for elements.
|
|
72
|
+
* Uses Playwright's internal aria-ref selector engine.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const { snapshot, getRefsForLocators } = await getAriaSnapshot({ page })
|
|
77
|
+
* const refs = await getRefsForLocators([page.locator('button'), page.locator('a')])
|
|
78
|
+
* // refs[0].ref is e.g. "e5" - use page.locator('aria-ref=e5') to select
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export async function getAriaSnapshot({ page }) {
|
|
82
|
+
const snapshotMethod = page._snapshotForAI;
|
|
83
|
+
if (!snapshotMethod) {
|
|
84
|
+
throw new Error('_snapshotForAI not available. Ensure you are using Playwright.');
|
|
85
|
+
}
|
|
86
|
+
const snapshot = await snapshotMethod.call(page);
|
|
87
|
+
const snapshotStr = typeof snapshot === 'string' ? snapshot : (snapshot.full || JSON.stringify(snapshot, null, 2));
|
|
88
|
+
// Discover refs by probing aria-ref=e1, e2, e3... until 10 consecutive misses
|
|
89
|
+
const refToElement = new Map();
|
|
90
|
+
const refHandles = [];
|
|
91
|
+
let consecutiveMisses = 0;
|
|
92
|
+
let refNum = 1;
|
|
93
|
+
while (consecutiveMisses < 10) {
|
|
94
|
+
const ref = `e${refNum++}`;
|
|
95
|
+
try {
|
|
96
|
+
const locator = page.locator(`aria-ref=${ref}`);
|
|
97
|
+
if (await locator.count() === 1) {
|
|
98
|
+
consecutiveMisses = 0;
|
|
99
|
+
const [info, handle] = await Promise.all([
|
|
100
|
+
locator.evaluate((el) => ({
|
|
101
|
+
role: el.getAttribute('role') || {
|
|
102
|
+
a: el.hasAttribute('href') ? 'link' : 'generic',
|
|
103
|
+
button: 'button', input: { button: 'button', checkbox: 'checkbox', radio: 'radio',
|
|
104
|
+
text: 'textbox', search: 'searchbox', number: 'spinbutton', range: 'slider',
|
|
105
|
+
}[el.type] || 'textbox', select: 'combobox', textarea: 'textbox', img: 'img',
|
|
106
|
+
nav: 'navigation', main: 'main', header: 'banner', footer: 'contentinfo',
|
|
107
|
+
}[el.tagName.toLowerCase()] || 'generic',
|
|
108
|
+
name: el.getAttribute('aria-label') || el.textContent?.trim() || el.placeholder || '',
|
|
109
|
+
})),
|
|
110
|
+
locator.elementHandle({ timeout: 1000 }),
|
|
111
|
+
]);
|
|
112
|
+
refToElement.set(ref, info);
|
|
113
|
+
if (handle) {
|
|
114
|
+
refHandles.push({ ref, handle });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
consecutiveMisses++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
consecutiveMisses++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Find refs for multiple locators in a single evaluate call
|
|
126
|
+
const getRefsForLocators = async (locators) => {
|
|
127
|
+
if (locators.length === 0 || refHandles.length === 0) {
|
|
128
|
+
return locators.map(() => null);
|
|
129
|
+
}
|
|
130
|
+
const targetHandles = await Promise.all(locators.map(async (loc) => {
|
|
131
|
+
try {
|
|
132
|
+
return 'elementHandle' in loc
|
|
133
|
+
? await loc.elementHandle({ timeout: 1000 })
|
|
134
|
+
: loc;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}));
|
|
140
|
+
const matchingRefs = await page.evaluate(({ targets, candidates }) => targets.map((target) => {
|
|
141
|
+
if (!target)
|
|
142
|
+
return null;
|
|
143
|
+
return candidates.find(({ element }) => element === target)?.ref ?? null;
|
|
144
|
+
}), { targets: targetHandles, candidates: refHandles.map(({ ref, handle }) => ({ ref, element: handle })) });
|
|
145
|
+
return matchingRefs.map((ref) => {
|
|
146
|
+
if (!ref)
|
|
147
|
+
return null;
|
|
148
|
+
const info = refToElement.get(ref);
|
|
149
|
+
return info ? { ...info, ref } : null;
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
snapshot: snapshotStr,
|
|
154
|
+
refToElement,
|
|
155
|
+
refHandles,
|
|
156
|
+
getRefsForLocators,
|
|
157
|
+
getRefForLocator: async (loc) => (await getRefsForLocators([loc]))[0],
|
|
158
|
+
getRefStringForLocator: async (loc) => (await getRefsForLocators([loc]))[0]?.ref ?? null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Show Vimium-style labels on interactive elements.
|
|
163
|
+
* Labels are yellow badges positioned above each element showing the aria ref (e.g., "e1", "e2").
|
|
164
|
+
* Use with screenshots so agents can see which elements are interactive.
|
|
165
|
+
*
|
|
166
|
+
* Labels auto-hide after 5 seconds to prevent stale labels remaining on the page.
|
|
167
|
+
* Call this function again if the page HTML changes to get fresh labels.
|
|
168
|
+
*
|
|
169
|
+
* By default, only shows labels for truly interactive roles (button, link, textbox, etc.)
|
|
170
|
+
* to reduce visual clutter. Set `interactiveOnly: false` to show all elements with refs.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```ts
|
|
174
|
+
* const { snapshot, labelCount } = await showAriaRefLabels({ page })
|
|
175
|
+
* await page.screenshot({ path: '/tmp/screenshot.png' })
|
|
176
|
+
* // Agent sees [e5] label on "Submit" button
|
|
177
|
+
* await page.locator('aria-ref=e5').click()
|
|
178
|
+
* // Labels auto-hide after 5 seconds, or call hideAriaRefLabels() manually
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export async function showAriaRefLabels({ page, interactiveOnly = true }) {
|
|
182
|
+
const { snapshot, refHandles, refToElement } = await getAriaSnapshot({ page });
|
|
183
|
+
// Filter to only interactive elements if requested
|
|
184
|
+
const filteredRefs = interactiveOnly
|
|
185
|
+
? refHandles.filter(({ ref }) => {
|
|
186
|
+
const info = refToElement.get(ref);
|
|
187
|
+
return info && INTERACTIVE_ROLES.has(info.role);
|
|
188
|
+
})
|
|
189
|
+
: refHandles;
|
|
190
|
+
// Build refs with role info for color coding
|
|
191
|
+
const refsWithRoles = filteredRefs.map(({ ref, handle }) => ({
|
|
192
|
+
ref,
|
|
193
|
+
element: handle,
|
|
194
|
+
role: refToElement.get(ref)?.role || 'generic',
|
|
195
|
+
}));
|
|
196
|
+
// Single evaluate call: create container, styles, and all labels
|
|
197
|
+
// ElementHandles get unwrapped to DOM elements in browser context
|
|
198
|
+
// Using 'any' types here since this code runs in browser context
|
|
199
|
+
const labelCount = await page.evaluate(
|
|
200
|
+
// Using 'any' for browser types since this runs in browser context
|
|
201
|
+
({ refs, containerId, containerStyles, labelStyles, roleColors, defaultColors }) => {
|
|
202
|
+
const doc = globalThis.document;
|
|
203
|
+
const win = globalThis;
|
|
204
|
+
// Cancel any pending auto-hide timer from previous call
|
|
205
|
+
const timerKey = '__playwriter_labels_timer__';
|
|
206
|
+
if (win[timerKey]) {
|
|
207
|
+
win.clearTimeout(win[timerKey]);
|
|
208
|
+
win[timerKey] = null;
|
|
209
|
+
}
|
|
210
|
+
// Remove existing labels if present (idempotent)
|
|
211
|
+
doc.getElementById(containerId)?.remove();
|
|
212
|
+
// Create container - absolute positioned, max z-index, no pointer events
|
|
213
|
+
const container = doc.createElement('div');
|
|
214
|
+
container.id = containerId;
|
|
215
|
+
container.style.cssText = containerStyles;
|
|
216
|
+
// Inject base label CSS
|
|
217
|
+
const style = doc.createElement('style');
|
|
218
|
+
style.textContent = labelStyles;
|
|
219
|
+
container.appendChild(style);
|
|
220
|
+
// Track placed label rectangles for overlap detection
|
|
221
|
+
// Each rect is { left, top, right, bottom } in viewport coordinates
|
|
222
|
+
const placedLabels = [];
|
|
223
|
+
// Estimate label dimensions (11px font + padding)
|
|
224
|
+
const LABEL_HEIGHT = 16;
|
|
225
|
+
const LABEL_CHAR_WIDTH = 7; // approximate width per character
|
|
226
|
+
// Parse alpha from rgb/rgba color string (getComputedStyle always returns these formats)
|
|
227
|
+
const getColorAlpha = (color) => {
|
|
228
|
+
if (color === 'transparent')
|
|
229
|
+
return 0;
|
|
230
|
+
// Match rgba(r, g, b, a) or rgb(r, g, b)
|
|
231
|
+
const match = color.match(/rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*(?:,\s*([\d.]+)\s*)?\)/);
|
|
232
|
+
if (match) {
|
|
233
|
+
return match[1] !== undefined ? parseFloat(match[1]) : 1;
|
|
234
|
+
}
|
|
235
|
+
return 1; // Default to opaque for unrecognized formats
|
|
236
|
+
};
|
|
237
|
+
// Check if an element has an opaque background that would block elements behind it
|
|
238
|
+
const isOpaqueElement = (el) => {
|
|
239
|
+
const style = win.getComputedStyle(el);
|
|
240
|
+
// Check element opacity
|
|
241
|
+
const opacity = parseFloat(style.opacity);
|
|
242
|
+
if (opacity < 0.1)
|
|
243
|
+
return false;
|
|
244
|
+
// Check background-color alpha
|
|
245
|
+
const bgAlpha = getColorAlpha(style.backgroundColor);
|
|
246
|
+
if (bgAlpha > 0.1)
|
|
247
|
+
return true;
|
|
248
|
+
// Check if has background-image (usually opaque)
|
|
249
|
+
if (style.backgroundImage !== 'none')
|
|
250
|
+
return true;
|
|
251
|
+
return false;
|
|
252
|
+
};
|
|
253
|
+
// Check if element is visible (not covered by opaque overlay)
|
|
254
|
+
const isElementVisible = (element, rect) => {
|
|
255
|
+
const centerX = rect.left + rect.width / 2;
|
|
256
|
+
const centerY = rect.top + rect.height / 2;
|
|
257
|
+
// Get all elements at this point, from top to bottom
|
|
258
|
+
const stack = doc.elementsFromPoint(centerX, centerY);
|
|
259
|
+
// Find our target element in the stack
|
|
260
|
+
const targetIndex = stack.findIndex((el) => element.contains(el) || el.contains(element));
|
|
261
|
+
// Element not in stack at all - not visible
|
|
262
|
+
if (targetIndex === -1)
|
|
263
|
+
return false;
|
|
264
|
+
// Check if any opaque element is above our target
|
|
265
|
+
for (let i = 0; i < targetIndex; i++) {
|
|
266
|
+
const el = stack[i];
|
|
267
|
+
// Skip our own overlay container
|
|
268
|
+
if (el.id === containerId)
|
|
269
|
+
continue;
|
|
270
|
+
// Skip pointer-events: none elements (decorative overlays)
|
|
271
|
+
if (win.getComputedStyle(el).pointerEvents === 'none')
|
|
272
|
+
continue;
|
|
273
|
+
// If this element is opaque, our target is blocked
|
|
274
|
+
if (isOpaqueElement(el))
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
// Check if two rectangles overlap
|
|
280
|
+
const rectsOverlap = (a, b) => {
|
|
281
|
+
return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
|
|
282
|
+
};
|
|
283
|
+
// Create label for each interactive element
|
|
284
|
+
let count = 0;
|
|
285
|
+
for (const { ref, role, element } of refs) {
|
|
286
|
+
const rect = element.getBoundingClientRect();
|
|
287
|
+
// Skip elements with no size (hidden)
|
|
288
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
// Skip elements that are covered by opaque overlays
|
|
292
|
+
if (!isElementVisible(element, rect)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
// Calculate label position and dimensions
|
|
296
|
+
const labelWidth = ref.length * LABEL_CHAR_WIDTH + 8; // +8 for padding
|
|
297
|
+
const labelLeft = rect.left;
|
|
298
|
+
const labelTop = Math.max(0, rect.top - LABEL_HEIGHT);
|
|
299
|
+
const labelRect = {
|
|
300
|
+
left: labelLeft,
|
|
301
|
+
top: labelTop,
|
|
302
|
+
right: labelLeft + labelWidth,
|
|
303
|
+
bottom: labelTop + LABEL_HEIGHT,
|
|
304
|
+
};
|
|
305
|
+
// Skip if this label would overlap with any already-placed label
|
|
306
|
+
const overlaps = placedLabels.some((placed) => rectsOverlap(labelRect, placed));
|
|
307
|
+
if (overlaps) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
// Get colors for this role
|
|
311
|
+
const [gradTop, gradBottom, border] = roleColors[role] || defaultColors;
|
|
312
|
+
// Place the label
|
|
313
|
+
const label = doc.createElement('div');
|
|
314
|
+
label.className = '__pw_label__';
|
|
315
|
+
label.textContent = ref;
|
|
316
|
+
label.style.background = `linear-gradient(to bottom, ${gradTop} 0%, ${gradBottom} 100%)`;
|
|
317
|
+
label.style.border = `1px solid ${border}`;
|
|
318
|
+
// Position above element, accounting for scroll
|
|
319
|
+
label.style.left = `${win.scrollX + labelLeft}px`;
|
|
320
|
+
label.style.top = `${win.scrollY + labelTop}px`;
|
|
321
|
+
container.appendChild(label);
|
|
322
|
+
placedLabels.push(labelRect);
|
|
323
|
+
count++;
|
|
324
|
+
}
|
|
325
|
+
doc.documentElement.appendChild(container);
|
|
326
|
+
// Auto-hide labels after 5 seconds to prevent stale labels
|
|
327
|
+
// Store timer ID so it can be cancelled if showAriaRefLabels is called again
|
|
328
|
+
win[timerKey] = win.setTimeout(() => {
|
|
329
|
+
doc.getElementById(containerId)?.remove();
|
|
330
|
+
win[timerKey] = null;
|
|
331
|
+
}, 5000);
|
|
332
|
+
return count;
|
|
333
|
+
}, {
|
|
334
|
+
refs: refsWithRoles.map(({ ref, role, element }) => ({ ref, role, element })),
|
|
335
|
+
containerId: LABELS_CONTAINER_ID,
|
|
336
|
+
containerStyles: CONTAINER_STYLES,
|
|
337
|
+
labelStyles: LABEL_STYLES,
|
|
338
|
+
roleColors: ROLE_COLORS,
|
|
339
|
+
defaultColors: DEFAULT_COLORS,
|
|
340
|
+
});
|
|
341
|
+
return { snapshot, labelCount };
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Remove all aria ref labels from the page.
|
|
345
|
+
*/
|
|
346
|
+
export async function hideAriaRefLabels({ page }) {
|
|
347
|
+
await page.evaluate((id) => {
|
|
348
|
+
const doc = globalThis.document;
|
|
349
|
+
const win = globalThis;
|
|
350
|
+
// Cancel any pending auto-hide timer
|
|
351
|
+
const timerKey = '__playwriter_labels_timer__';
|
|
352
|
+
if (win[timerKey]) {
|
|
353
|
+
win.clearTimeout(win[timerKey]);
|
|
354
|
+
win[timerKey] = null;
|
|
355
|
+
}
|
|
356
|
+
doc.getElementById(id)?.remove();
|
|
357
|
+
}, LABELS_CONTAINER_ID);
|
|
358
|
+
}
|
|
359
|
+
//# sourceMappingURL=aria-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aria-snapshot.js","sourceRoot":"","sources":["../src/aria-snapshot.ts"],"names":[],"mappings":"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"}
|
package/dist/cdp-relay.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cdp-relay.d.ts","sourceRoot":"","sources":["../src/cdp-relay.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAA0D,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAoB/G,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,IAAI,IAAI,CAAA;IACb,EAAE,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IACrF,GAAG,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;CACvF,CAAA;AAED,wBAAsB,6BAA6B,CAAC,EAAE,IAAY,EAAE,IAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;KAAE,CAAA;CAAO,GAAG,OAAO,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"cdp-relay.d.ts","sourceRoot":"","sources":["../src/cdp-relay.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAA0D,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAoB/G,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,IAAI,IAAI,CAAA;IACb,EAAE,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IACrF,GAAG,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;CACvF,CAAA;AAED,wBAAsB,6BAA6B,CAAC,EAAE,IAAY,EAAE,IAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;KAAE,CAAA;CAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAyyBzP"}
|
package/dist/cdp-relay.js
CHANGED
|
@@ -11,6 +11,21 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
11
11
|
let extensionWs = null;
|
|
12
12
|
const extensionPendingRequests = new Map();
|
|
13
13
|
let extensionMessageId = 0;
|
|
14
|
+
let extensionPingInterval = null;
|
|
15
|
+
function startExtensionPing() {
|
|
16
|
+
if (extensionPingInterval) {
|
|
17
|
+
clearInterval(extensionPingInterval);
|
|
18
|
+
}
|
|
19
|
+
extensionPingInterval = setInterval(() => {
|
|
20
|
+
extensionWs?.send(JSON.stringify({ method: 'ping' }));
|
|
21
|
+
}, 5000);
|
|
22
|
+
}
|
|
23
|
+
function stopExtensionPing() {
|
|
24
|
+
if (extensionPingInterval) {
|
|
25
|
+
clearInterval(extensionPingInterval);
|
|
26
|
+
extensionPingInterval = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
14
29
|
function logCdpMessage({ direction, clientId, method, sessionId, params, id, source }) {
|
|
15
30
|
const noisyEvents = [
|
|
16
31
|
'Network.requestWillBeSentExtraInfo',
|
|
@@ -107,6 +122,34 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
107
122
|
});
|
|
108
123
|
});
|
|
109
124
|
}
|
|
125
|
+
// Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
|
|
126
|
+
// This allows Playwright to connect and immediately have a page to work with.
|
|
127
|
+
async function maybeAutoCreateInitialTab() {
|
|
128
|
+
if (!process.env.PLAYWRITER_AUTO_ENABLE) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!extensionWs) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (connectedTargets.size > 0) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
logger?.log(chalk.blue('Auto-creating initial tab for Playwright client'));
|
|
139
|
+
const result = await sendToExtension({ method: 'createInitialTab', timeout: 10000 });
|
|
140
|
+
if (result.success && result.sessionId && result.targetInfo) {
|
|
141
|
+
connectedTargets.set(result.sessionId, {
|
|
142
|
+
sessionId: result.sessionId,
|
|
143
|
+
targetId: result.targetInfo.targetId,
|
|
144
|
+
targetInfo: result.targetInfo
|
|
145
|
+
});
|
|
146
|
+
logger?.log(chalk.blue(`Auto-created tab, now have ${connectedTargets.size} targets, url: ${result.targetInfo.url}`));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
logger?.error('Failed to auto-create initial tab:', e);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
110
153
|
async function routeCdpCommand({ method, params, sessionId }) {
|
|
111
154
|
switch (method) {
|
|
112
155
|
case 'Browser.getVersion': {
|
|
@@ -121,10 +164,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
121
164
|
case 'Browser.setDownloadBehavior': {
|
|
122
165
|
return {};
|
|
123
166
|
}
|
|
167
|
+
// Target.setAutoAttach is a CDP command Playwright sends on first connection.
|
|
168
|
+
// We use it as the hook to auto-create an initial tab. If Playwright changes
|
|
169
|
+
// its initialization sequence in the future, this could be moved to a different command.
|
|
124
170
|
case 'Target.setAutoAttach': {
|
|
125
171
|
if (sessionId) {
|
|
126
172
|
break;
|
|
127
173
|
}
|
|
174
|
+
await maybeAutoCreateInitialTab();
|
|
128
175
|
return {};
|
|
129
176
|
}
|
|
130
177
|
case 'Target.setDiscoverTargets': {
|
|
@@ -238,7 +285,25 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
238
285
|
return c.json({ ok: false }, 400);
|
|
239
286
|
}
|
|
240
287
|
});
|
|
288
|
+
// Validate Origin header for WebSocket connections to prevent cross-origin attacks.
|
|
289
|
+
// Browsers always send Origin header for WebSocket connections, but Node.js clients don't.
|
|
290
|
+
// We reject browser origins (except chrome-extension://) to prevent malicious websites
|
|
291
|
+
// from connecting to the local WebSocket server.
|
|
292
|
+
function isAllowedOrigin(origin) {
|
|
293
|
+
if (!origin) {
|
|
294
|
+
return true; // Node.js clients don't send Origin
|
|
295
|
+
}
|
|
296
|
+
if (origin.startsWith('chrome-extension://')) {
|
|
297
|
+
return true; // Chrome extension is allowed
|
|
298
|
+
}
|
|
299
|
+
return false; // Reject browser origins (http://, https://, etc.)
|
|
300
|
+
}
|
|
241
301
|
app.get('/cdp/:clientId?', (c, next) => {
|
|
302
|
+
const origin = c.req.header('origin');
|
|
303
|
+
if (!isAllowedOrigin(origin)) {
|
|
304
|
+
logger?.log(chalk.red(`Rejecting /cdp WebSocket from origin: ${origin}`));
|
|
305
|
+
return c.text('Forbidden', 403);
|
|
306
|
+
}
|
|
242
307
|
if (token) {
|
|
243
308
|
const url = new URL(c.req.url, 'http://localhost');
|
|
244
309
|
const providedToken = url.searchParams.get('token');
|
|
@@ -250,14 +315,15 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
250
315
|
}, upgradeWebSocket((c) => {
|
|
251
316
|
const clientId = c.req.param('clientId') || 'default';
|
|
252
317
|
return {
|
|
253
|
-
onOpen(_event, ws) {
|
|
318
|
+
async onOpen(_event, ws) {
|
|
254
319
|
if (playwrightClients.has(clientId)) {
|
|
255
320
|
logger?.log(chalk.red(`Rejecting duplicate client ID: ${clientId}`));
|
|
256
321
|
ws.close(1000, 'Client ID already connected');
|
|
257
322
|
return;
|
|
258
323
|
}
|
|
324
|
+
// Add client first so it can receive Target.attachedToTarget events
|
|
259
325
|
playwrightClients.set(clientId, { id: clientId, ws });
|
|
260
|
-
logger?.log(chalk.green(`Playwright client connected: ${clientId} (${playwrightClients.size} total)`));
|
|
326
|
+
logger?.log(chalk.green(`Playwright client connected: ${clientId} (${playwrightClients.size} total) (extension? ${!!extensionWs}) (${connectedTargets.size} pages)`));
|
|
261
327
|
},
|
|
262
328
|
async onMessage(event, ws) {
|
|
263
329
|
let message;
|
|
@@ -302,6 +368,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
302
368
|
waitingForDebugger: false
|
|
303
369
|
}
|
|
304
370
|
};
|
|
371
|
+
if (!target.targetInfo.url) {
|
|
372
|
+
logger?.error(chalk.red('[Server] WARNING: Target.attachedToTarget sent with empty URL!'), JSON.stringify(attachedPayload));
|
|
373
|
+
}
|
|
305
374
|
logger?.log(chalk.magenta('[Server] Target.attachedToTarget full payload:'), JSON.stringify(attachedPayload));
|
|
306
375
|
sendToPlaywright({
|
|
307
376
|
message: attachedPayload,
|
|
@@ -321,6 +390,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
321
390
|
}
|
|
322
391
|
}
|
|
323
392
|
};
|
|
393
|
+
if (!target.targetInfo.url) {
|
|
394
|
+
logger?.error(chalk.red('[Server] WARNING: Target.targetCreated sent with empty URL!'), JSON.stringify(targetCreatedPayload));
|
|
395
|
+
}
|
|
324
396
|
logger?.log(chalk.magenta('[Server] Target.targetCreated full payload:'), JSON.stringify(targetCreatedPayload));
|
|
325
397
|
sendToPlaywright({
|
|
326
398
|
message: targetCreatedPayload,
|
|
@@ -344,6 +416,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
344
416
|
waitingForDebugger: false
|
|
345
417
|
}
|
|
346
418
|
};
|
|
419
|
+
if (!target.targetInfo.url) {
|
|
420
|
+
logger?.error(chalk.red('[Server] WARNING: Target.attachedToTarget (from attachToTarget) sent with empty URL!'), JSON.stringify(attachedPayload));
|
|
421
|
+
}
|
|
347
422
|
logger?.log(chalk.magenta('[Server] Target.attachedToTarget (from attachToTarget) payload:'), JSON.stringify(attachedPayload));
|
|
348
423
|
sendToPlaywright({
|
|
349
424
|
message: attachedPayload,
|
|
@@ -376,7 +451,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
376
451
|
}
|
|
377
452
|
};
|
|
378
453
|
}));
|
|
379
|
-
app.get('/extension',
|
|
454
|
+
app.get('/extension', (c, next) => {
|
|
455
|
+
const origin = c.req.header('origin');
|
|
456
|
+
if (!isAllowedOrigin(origin)) {
|
|
457
|
+
logger?.log(chalk.red(`Rejecting /extension WebSocket from origin: ${origin}`));
|
|
458
|
+
return c.text('Forbidden', 403);
|
|
459
|
+
}
|
|
460
|
+
return next();
|
|
461
|
+
}, upgradeWebSocket(() => {
|
|
380
462
|
return {
|
|
381
463
|
onOpen(_event, ws) {
|
|
382
464
|
if (extensionWs) {
|
|
@@ -394,6 +476,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
394
476
|
playwrightClients.clear();
|
|
395
477
|
}
|
|
396
478
|
extensionWs = ws;
|
|
479
|
+
startExtensionPing();
|
|
397
480
|
logger?.log('Extension connected with clean state');
|
|
398
481
|
},
|
|
399
482
|
async onMessage(event, ws) {
|
|
@@ -405,7 +488,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
405
488
|
ws.close(1000, 'Invalid JSON');
|
|
406
489
|
return;
|
|
407
490
|
}
|
|
408
|
-
if (
|
|
491
|
+
if (message.id !== undefined) {
|
|
409
492
|
const pending = extensionPendingRequests.get(message.id);
|
|
410
493
|
if (!pending) {
|
|
411
494
|
logger?.log('Unexpected response with id:', message.id);
|
|
@@ -419,6 +502,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
419
502
|
pending.resolve(message.result);
|
|
420
503
|
}
|
|
421
504
|
}
|
|
505
|
+
else if (message.method === 'pong') {
|
|
506
|
+
// Keep-alive response, nothing to do
|
|
507
|
+
}
|
|
422
508
|
else if (message.method === 'log') {
|
|
423
509
|
const { level, args } = message.params;
|
|
424
510
|
const logFn = logger?.[level] || logger?.log;
|
|
@@ -441,6 +527,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
441
527
|
emitter.emit('cdp:event', { event: cdpEvent, sessionId });
|
|
442
528
|
if (method === 'Target.attachedToTarget') {
|
|
443
529
|
const targetParams = params;
|
|
530
|
+
if (!targetParams.targetInfo.url) {
|
|
531
|
+
logger?.error(chalk.red('[Extension] WARNING: Target.attachedToTarget received with empty URL!'), JSON.stringify({ method, params: targetParams, sessionId }));
|
|
532
|
+
}
|
|
444
533
|
logger?.log(chalk.yellow('[Extension] Target.attachedToTarget full payload:'), JSON.stringify({ method, params: targetParams, sessionId }));
|
|
445
534
|
// Check if we already sent this target to clients (e.g., from Target.setAutoAttach response)
|
|
446
535
|
const alreadyConnected = connectedTargets.has(targetParams.sessionId);
|
|
@@ -561,7 +650,8 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
561
650
|
}
|
|
562
651
|
},
|
|
563
652
|
onClose(event, ws) {
|
|
564
|
-
logger?.log(
|
|
653
|
+
logger?.log(`Extension disconnected: code=${event.code} reason=${event.reason || 'none'}`);
|
|
654
|
+
stopExtensionPing();
|
|
565
655
|
// If this is an old connection closing after we've already established a new one,
|
|
566
656
|
// don't clear the global state
|
|
567
657
|
if (extensionWs && extensionWs !== ws) {
|