dsh-browser-playwright-codex 0.5.1 → 0.5.3
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/CHANGELOG.md +54 -1
- package/lib/browser-lifecycle.d.ts +12 -0
- package/lib/browser-lifecycle.js +186 -0
- package/lib/challenge.d.ts +20 -0
- package/lib/challenge.js +40 -0
- package/lib/config.d.ts +81 -0
- package/lib/config.js +61 -0
- package/lib/extract.d.ts +13 -0
- package/lib/extract.js +34 -0
- package/lib/index.d.ts +4 -2
- package/lib/index.js +3 -1
- package/lib/login-state.d.ts +10 -0
- package/lib/login-state.js +83 -0
- package/lib/page-actions.d.ts +134 -0
- package/lib/page-actions.js +725 -0
- package/lib/page-diagnostics.d.ts +65 -0
- package/lib/page-diagnostics.js +181 -0
- package/lib/page-probes.d.ts +78 -0
- package/lib/page-probes.js +281 -0
- package/lib/page-snapshot.d.ts +96 -0
- package/lib/page-snapshot.js +280 -0
- package/lib/playwright.d.ts +5 -636
- package/lib/playwright.js +28 -2494
- package/lib/provider.d.ts +157 -0
- package/lib/provider.js +352 -0
- package/lib/session.d.ts +410 -0
- package/lib/session.js +633 -0
- package/lib/tool-render.d.ts +20 -0
- package/lib/tool-render.js +137 -0
- package/lib/tool-schemas.d.ts +76 -0
- package/lib/tool-schemas.js +46 -0
- package/lib/tool-types.d.ts +82 -0
- package/lib/tool-types.js +1 -0
- package/lib/tool.d.ts +2 -26
- package/lib/tool.js +5 -252
- package/lib/url-policy.d.ts +2 -0
- package/lib/url-policy.js +26 -0
- package/lib/window-focus.d.ts +27 -0
- package/lib/window-focus.js +54 -0
- package/package.json +4 -4
- package/scripts/dsh-browser-verify.mjs +2 -1
- package/scripts/prompt-cost.mjs +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 读取网页并生成快照:等页面稳定、兼容(注入脚本)与智能(aria)两种采集、ref 签名与快照差异、
|
|
3
|
+
* 以及 ref 到定位器的解析与存活校验。
|
|
4
|
+
* @module dsh-browser-playwright-codex/page-snapshot
|
|
5
|
+
*/
|
|
6
|
+
import type { Page } from 'playwright-core';
|
|
7
|
+
import type { PlaywrightSession } from './session.ts';
|
|
8
|
+
import type { BrowserNode, BrowserSnapshot } from './types.ts';
|
|
9
|
+
import type { SnapshotDiffEntry } from './types.ts';
|
|
10
|
+
/**
|
|
11
|
+
* Which page-reading mode a capture uses. The user's choice on the settings page
|
|
12
|
+
* (Settings → 浏览器 → 页面识别方式) wins over the deployment default, so flipping
|
|
13
|
+
* that switch changes the very next operation — no restart, no config edit.
|
|
14
|
+
* @param configured - the mode from this provider's config.
|
|
15
|
+
* @returns the effective mode.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveSnapshotEngine(configured: 'legacy' | 'aria'): 'legacy' | 'aria';
|
|
18
|
+
/** Feature fingerprint of one ref-bearing node, for cross-snapshot diffing. */
|
|
19
|
+
export interface RefSignature {
|
|
20
|
+
role?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
level?: number;
|
|
23
|
+
checked?: boolean;
|
|
24
|
+
selected?: boolean;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
href?: string;
|
|
27
|
+
parentRef?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Feature flags carried by an added diff entry. */
|
|
30
|
+
export declare function flagsOf(sig: RefSignature): string[];
|
|
31
|
+
/** Flag keys whose value changed between two signatures. */
|
|
32
|
+
export declare function flagsDeltaOf(before: RefSignature, after: RefSignature): string[];
|
|
33
|
+
/** Build a SnapshotDiffEntry, dropping undefined optionals (exactOptionalPropertyTypes). */
|
|
34
|
+
export declare function diffEntry(input: {
|
|
35
|
+
ref: string;
|
|
36
|
+
role?: string | undefined;
|
|
37
|
+
name?: string | undefined;
|
|
38
|
+
flags?: string[] | undefined;
|
|
39
|
+
flagsDelta?: string[] | undefined;
|
|
40
|
+
parentRef?: string | undefined;
|
|
41
|
+
}): SnapshotDiffEntry;
|
|
42
|
+
/** Post-action settle: wait out a real navigation, then render-stabilize before snapshotting. */
|
|
43
|
+
export declare function settleAndSnapshot(session: PlaywrightSession, page: Page, navChanged: boolean): Promise<BrowserSnapshot>;
|
|
44
|
+
/**
|
|
45
|
+
* Semantic-tree capture via Playwright's official ariaSnapshot(mode:'ai').
|
|
46
|
+
* An unavailable or unreadable aria tree (`missing`) degrades to the legacy
|
|
47
|
+
* DOM walker instead of returning an empty snapshot: the model must never
|
|
48
|
+
* receive "the page has no elements" because the engine failed to parse.
|
|
49
|
+
*/
|
|
50
|
+
export declare function captureAria(session: PlaywrightSession, page: Page, opts: {
|
|
51
|
+
interactiveOnly?: boolean;
|
|
52
|
+
} | undefined): Promise<{
|
|
53
|
+
engineNote: string;
|
|
54
|
+
url: string;
|
|
55
|
+
title: string;
|
|
56
|
+
nodes: BrowserNode[];
|
|
57
|
+
totalRefs: number;
|
|
58
|
+
truncated: boolean;
|
|
59
|
+
} | {
|
|
60
|
+
url: string;
|
|
61
|
+
title: string;
|
|
62
|
+
nodes: BrowserNode[];
|
|
63
|
+
totalRefs: number;
|
|
64
|
+
truncated: boolean;
|
|
65
|
+
}>;
|
|
66
|
+
/** Legacy capture: injected DOM walker (default engine, byte-identical output). */
|
|
67
|
+
export declare function captureLegacy(session: PlaywrightSession, page: Page, opts: {
|
|
68
|
+
interactiveOnly?: boolean;
|
|
69
|
+
} | undefined): Promise<{
|
|
70
|
+
url: string;
|
|
71
|
+
title: string;
|
|
72
|
+
nodes: BrowserNode[];
|
|
73
|
+
totalRefs: number;
|
|
74
|
+
truncated: boolean;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Attach an incremental diff when snapshot.diff is enabled. Absence of the
|
|
78
|
+
* diff field means "full snapshot follows": first capture of the session,
|
|
79
|
+
* navigation reset, or a truncated tree. The diff is keyed by the stable
|
|
80
|
+
* data-dsh-ref, so legacy and aria engines share the same contract.
|
|
81
|
+
*/
|
|
82
|
+
export declare function attachDiff(session: PlaywrightSession, snap: BrowserSnapshot): BrowserSnapshot;
|
|
83
|
+
/** Build ref -> feature signature map from a captured tree, with parent refs. */
|
|
84
|
+
export declare function buildRefSignatures(_session: PlaywrightSession, nodes: readonly BrowserNode[]): Map<string, RefSignature>;
|
|
85
|
+
export declare function snapshot(session: PlaywrightSession, opts?: {
|
|
86
|
+
interactiveOnly?: boolean;
|
|
87
|
+
}): Promise<BrowserSnapshot | {
|
|
88
|
+
url: string;
|
|
89
|
+
title: string;
|
|
90
|
+
nodes: never[];
|
|
91
|
+
totalRefs: number;
|
|
92
|
+
truncated: boolean;
|
|
93
|
+
dialogNote: string;
|
|
94
|
+
}>;
|
|
95
|
+
export declare function refLocator(session: PlaywrightSession, ref: string): Promise<import("playwright-core").Locator>;
|
|
96
|
+
export declare function assertLive(session: PlaywrightSession): void;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { captureAriaSnapshot } from "./snapshot-aria.js";
|
|
2
|
+
import { isContextDestroyed } from "./page-diagnostics.js";
|
|
3
|
+
import { REF_PATTERN } from "./config.js";
|
|
4
|
+
import { BrowserError } from "./errors.js";
|
|
5
|
+
import { getSnapshotEngine } from "./runtime-state.js";
|
|
6
|
+
/**
|
|
7
|
+
* Which page-reading mode a capture uses. The user's choice on the settings page
|
|
8
|
+
* (Settings → 浏览器 → 页面识别方式) wins over the deployment default, so flipping
|
|
9
|
+
* that switch changes the very next operation — no restart, no config edit.
|
|
10
|
+
* @param configured - the mode from this provider's config.
|
|
11
|
+
* @returns the effective mode.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveSnapshotEngine(configured) {
|
|
14
|
+
return getSnapshotEngine() ?? configured;
|
|
15
|
+
}
|
|
16
|
+
/** Feature flags carried by an added diff entry. */
|
|
17
|
+
export function flagsOf(sig) {
|
|
18
|
+
const flags = [];
|
|
19
|
+
if (sig.checked === true)
|
|
20
|
+
flags.push('checked');
|
|
21
|
+
if (sig.selected === true)
|
|
22
|
+
flags.push('selected');
|
|
23
|
+
if (sig.disabled === true)
|
|
24
|
+
flags.push('disabled');
|
|
25
|
+
if (sig.level !== undefined)
|
|
26
|
+
flags.push('level=' + String(sig.level));
|
|
27
|
+
if (sig.href !== undefined)
|
|
28
|
+
flags.push('href=' + sig.href);
|
|
29
|
+
return flags;
|
|
30
|
+
}
|
|
31
|
+
/** Flag keys whose value changed between two signatures. */
|
|
32
|
+
export function flagsDeltaOf(before, after) {
|
|
33
|
+
const delta = [];
|
|
34
|
+
const keys = ['checked', 'selected', 'disabled', 'level', 'href'];
|
|
35
|
+
for (const key of keys) {
|
|
36
|
+
const b = before[key];
|
|
37
|
+
const a = after[key];
|
|
38
|
+
if (String(b ?? '') === String(a ?? ''))
|
|
39
|
+
continue;
|
|
40
|
+
delta.push(a !== undefined && a !== false
|
|
41
|
+
? key === 'level'
|
|
42
|
+
? 'level=' + String(a)
|
|
43
|
+
: key === 'href'
|
|
44
|
+
? 'href=' + a
|
|
45
|
+
: key
|
|
46
|
+
: key);
|
|
47
|
+
}
|
|
48
|
+
return delta;
|
|
49
|
+
}
|
|
50
|
+
/** Build a SnapshotDiffEntry, dropping undefined optionals (exactOptionalPropertyTypes). */
|
|
51
|
+
export function diffEntry(input) {
|
|
52
|
+
const out = { ref: input.ref };
|
|
53
|
+
if (input.role !== undefined)
|
|
54
|
+
out.role = input.role;
|
|
55
|
+
if (input.name !== undefined)
|
|
56
|
+
out.name = input.name;
|
|
57
|
+
if (input.flags !== undefined)
|
|
58
|
+
out.flags = input.flags;
|
|
59
|
+
if (input.flagsDelta !== undefined)
|
|
60
|
+
out.flagsDelta = input.flagsDelta;
|
|
61
|
+
if (input.parentRef !== undefined)
|
|
62
|
+
out.parentRef = input.parentRef;
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/** Post-action settle: wait out a real navigation, then render-stabilize before snapshotting. */
|
|
66
|
+
export async function settleAndSnapshot(session, page, navChanged) {
|
|
67
|
+
// Race the settle+capture against a fresh dialog: a chained dialog
|
|
68
|
+
// (one raised the moment the previous was answered) would block
|
|
69
|
+
// page.evaluate forever and hang session call.
|
|
70
|
+
const raced = await session.runAction(page, async () => {
|
|
71
|
+
if (navChanged) {
|
|
72
|
+
await page.waitForLoadState('load', { timeout: session.timeoutMs() }).catch(() => { });
|
|
73
|
+
}
|
|
74
|
+
await session.waitForPageStable(page, navChanged ? 1500 : 800);
|
|
75
|
+
return await session.capture(page, undefined);
|
|
76
|
+
});
|
|
77
|
+
if (raced.blocked)
|
|
78
|
+
return session.blockedSnapshot(page, raced.pending);
|
|
79
|
+
return raced.value;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Semantic-tree capture via Playwright's official ariaSnapshot(mode:'ai').
|
|
83
|
+
* An unavailable or unreadable aria tree (`missing`) degrades to the legacy
|
|
84
|
+
* DOM walker instead of returning an empty snapshot: the model must never
|
|
85
|
+
* receive "the page has no elements" because the engine failed to parse.
|
|
86
|
+
*/
|
|
87
|
+
export async function captureAria(session, page, opts) {
|
|
88
|
+
const o = {
|
|
89
|
+
interactiveOnly: opts?.interactiveOnly === true,
|
|
90
|
+
maxNodes: session.provider.config.snapshot.maxNodes,
|
|
91
|
+
maxNameLength: session.provider.config.snapshot.maxNameLength,
|
|
92
|
+
maxTextLength: session.provider.config.snapshot.maxTextLength,
|
|
93
|
+
};
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = await captureAriaSnapshot(page, o);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// Same mid-evaluate navigation race as the legacy path: settle once and retry.
|
|
100
|
+
if (!isContextDestroyed(error))
|
|
101
|
+
throw error;
|
|
102
|
+
await session.settleNavigation(page);
|
|
103
|
+
raw = await captureAriaSnapshot(page, o);
|
|
104
|
+
}
|
|
105
|
+
if (raw.missing === true) {
|
|
106
|
+
// Degrade honestly: the tree below is the compatible engine's, and the
|
|
107
|
+
// snapshot says so. Silence here is how a user who explicitly picked smart
|
|
108
|
+
// mode keeps believing smart mode is what they are reading.
|
|
109
|
+
const legacy = await session.captureLegacy(page, opts);
|
|
110
|
+
return {
|
|
111
|
+
...legacy,
|
|
112
|
+
engineNote: 'smart mode could not read session page, so it was rendered with the compatible engine. ' +
|
|
113
|
+
'The refs below belong to the compatible engine. Nothing is wrong with the page; ' +
|
|
114
|
+
'switch the page-reading mode in Settings if you want to stop seeing session.',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
url: page.url(),
|
|
119
|
+
title: await page.title(),
|
|
120
|
+
nodes: raw.nodes ?? [],
|
|
121
|
+
totalRefs: raw.totalRefs ?? 0,
|
|
122
|
+
truncated: raw.truncated ?? false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Legacy capture: injected DOM walker (default engine, byte-identical output). */
|
|
126
|
+
export async function captureLegacy(session, page, opts) {
|
|
127
|
+
const o = {
|
|
128
|
+
...(opts?.interactiveOnly !== undefined ? { interactiveOnly: opts.interactiveOnly } : {}),
|
|
129
|
+
maxNodes: session.provider.config.snapshot.maxNodes,
|
|
130
|
+
maxNameLength: session.provider.config.snapshot.maxNameLength,
|
|
131
|
+
maxTextLength: session.provider.config.snapshot.maxTextLength,
|
|
132
|
+
};
|
|
133
|
+
const read = (options) => {
|
|
134
|
+
const fn = window.__dshSnapshot;
|
|
135
|
+
if (typeof fn !== 'function')
|
|
136
|
+
return { nodes: [], truncated: false, totalRefs: 0, missing: true };
|
|
137
|
+
return fn(options);
|
|
138
|
+
};
|
|
139
|
+
let raw;
|
|
140
|
+
try {
|
|
141
|
+
raw = await page.evaluate(read, o);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
// A click can start a JS navigation that destroys the execution
|
|
145
|
+
// context mid-evaluate; wait for the new document and retry once.
|
|
146
|
+
if (!isContextDestroyed(error))
|
|
147
|
+
throw error;
|
|
148
|
+
await session.settleNavigation(page);
|
|
149
|
+
raw = await page.evaluate(read, o);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
url: page.url(),
|
|
153
|
+
title: await page.title(),
|
|
154
|
+
nodes: raw.nodes ?? [],
|
|
155
|
+
totalRefs: raw.totalRefs ?? 0,
|
|
156
|
+
truncated: raw.truncated ?? false,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Attach an incremental diff when snapshot.diff is enabled. Absence of the
|
|
161
|
+
* diff field means "full snapshot follows": first capture of the session,
|
|
162
|
+
* navigation reset, or a truncated tree. The diff is keyed by the stable
|
|
163
|
+
* data-dsh-ref, so legacy and aria engines share the same contract.
|
|
164
|
+
*/
|
|
165
|
+
export function attachDiff(session, snap) {
|
|
166
|
+
if (session.provider.config.snapshot.diff !== true) {
|
|
167
|
+
session.lastRefSignatures = undefined;
|
|
168
|
+
session.lastRefNonce = undefined;
|
|
169
|
+
return snap;
|
|
170
|
+
}
|
|
171
|
+
const sigs = session.buildRefSignatures(snap.nodes);
|
|
172
|
+
const nonce = session.refNonce(snap.nodes);
|
|
173
|
+
const base = session.lastRefSignatures;
|
|
174
|
+
const baseNonce = session.lastRefNonce;
|
|
175
|
+
session.lastRefSignatures = sigs;
|
|
176
|
+
session.lastRefNonce = nonce;
|
|
177
|
+
// No baseline yet (first capture) — the full snapshot is the baseline.
|
|
178
|
+
if (base === undefined || baseNonce === undefined)
|
|
179
|
+
return snap;
|
|
180
|
+
// Navigation reset (ref nonce changed) or truncation: the delta is void.
|
|
181
|
+
if (nonce !== baseNonce || snap.truncated === true) {
|
|
182
|
+
return { ...snap, diff: { added: [], removed: [], changed: [], same: 0, navigationReset: true } };
|
|
183
|
+
}
|
|
184
|
+
const added = [];
|
|
185
|
+
const removed = [];
|
|
186
|
+
const changed = [];
|
|
187
|
+
let same = 0;
|
|
188
|
+
for (const [ref, sig] of sigs) {
|
|
189
|
+
const oldSig = base.get(ref);
|
|
190
|
+
if (oldSig === undefined) {
|
|
191
|
+
added.push(diffEntry({ ref, role: sig.role, name: sig.name, flags: flagsOf(sig), parentRef: sig.parentRef }));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const delta = flagsDeltaOf(oldSig, sig);
|
|
195
|
+
if (sig.role !== oldSig.role || sig.name !== oldSig.name || delta.length > 0) {
|
|
196
|
+
changed.push(diffEntry({ ref, role: sig.role, name: sig.name, flagsDelta: delta, parentRef: sig.parentRef }));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
same += 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const ref of base.keys()) {
|
|
204
|
+
if (!sigs.has(ref))
|
|
205
|
+
removed.push(ref);
|
|
206
|
+
}
|
|
207
|
+
return { ...snap, diff: { added, removed, changed, same, navigationReset: false } };
|
|
208
|
+
}
|
|
209
|
+
/** Build ref -> feature signature map from a captured tree, with parent refs. */
|
|
210
|
+
export function buildRefSignatures(_session, nodes) {
|
|
211
|
+
const out = new Map();
|
|
212
|
+
const walk = (list, parentRef) => {
|
|
213
|
+
for (const node of list) {
|
|
214
|
+
if (node.ref !== undefined) {
|
|
215
|
+
out.set(node.ref, {
|
|
216
|
+
role: node.role,
|
|
217
|
+
name: node.name,
|
|
218
|
+
...(node.level !== undefined ? { level: node.level } : {}),
|
|
219
|
+
checked: node.checked === true,
|
|
220
|
+
selected: node.selected === true,
|
|
221
|
+
disabled: node.disabled === true,
|
|
222
|
+
...(node.href !== undefined ? { href: node.href } : {}),
|
|
223
|
+
...(parentRef !== undefined ? { parentRef } : {}),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
walk(node.children ?? [], node.ref ?? parentRef);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
walk(nodes);
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
export async function snapshot(session, opts) {
|
|
233
|
+
return session.run(async () => {
|
|
234
|
+
session.assertLive();
|
|
235
|
+
const page = await session.withDialogGuardLifted(() => session.ensurePage());
|
|
236
|
+
session.provider.touch(session.owner);
|
|
237
|
+
// A parked dialog blocks the page: never round-trip into it.
|
|
238
|
+
const pending = session.provider.pendingDialogFor(page);
|
|
239
|
+
if (pending !== undefined)
|
|
240
|
+
return session.blockedSnapshot(page, pending);
|
|
241
|
+
const racedSnap = await session.runAction(page, async () => {
|
|
242
|
+
await session.waitForPageStable(page, 400);
|
|
243
|
+
return await session.capture(page, opts);
|
|
244
|
+
});
|
|
245
|
+
if (racedSnap.blocked)
|
|
246
|
+
return session.blockedSnapshot(page, racedSnap.pending);
|
|
247
|
+
return racedSnap.value;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
export async function refLocator(session, ref) {
|
|
251
|
+
session.assertLive();
|
|
252
|
+
if (!REF_PATTERN.test(ref)) {
|
|
253
|
+
throw new BrowserError('REF_NOT_FOUND', 'invalid ref ' + JSON.stringify(ref) + ': use a ref from the latest browser_snapshot');
|
|
254
|
+
}
|
|
255
|
+
const locator = session
|
|
256
|
+
.currentPageSync()
|
|
257
|
+
.locator('[data-dsh-ref="' + ref + '"]')
|
|
258
|
+
.first();
|
|
259
|
+
// Fast-fail on stale refs (per-snapshot nonces make a reused number
|
|
260
|
+
// impossible) instead of burning the full action timeout.
|
|
261
|
+
if ((await locator.count()) === 0) {
|
|
262
|
+
throw new BrowserError('REF_NOT_FOUND', 'ref ' +
|
|
263
|
+
JSON.stringify(ref) +
|
|
264
|
+
' no longer matches an element; take a fresh browser_snapshot and use a ref from its result');
|
|
265
|
+
}
|
|
266
|
+
return locator;
|
|
267
|
+
}
|
|
268
|
+
export function assertLive(session) {
|
|
269
|
+
if (session.closed) {
|
|
270
|
+
throw new BrowserError('SESSION_CLOSED', 'the browser session was closed via browser_close; the next browser call opens a fresh one');
|
|
271
|
+
}
|
|
272
|
+
if (!session.provider.isLive(session.owner)) {
|
|
273
|
+
// The window went away without session session asking for it: idle
|
|
274
|
+
// disposal, an external manual close, or a crash. Surface it as a
|
|
275
|
+
// crash so the model retries — the next call relaunches with the
|
|
276
|
+
// saved login state instead of reporting a misleading close.
|
|
277
|
+
session.closed = true;
|
|
278
|
+
throw new BrowserError('BROWSER_CRASHED', 'the browser window is no longer available (idle timeout, manual close, or a crash); the next browser call reopens it with the saved login state');
|
|
279
|
+
}
|
|
280
|
+
}
|