@poe-platform/safe-bash 0.1.680 → 0.1.682
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/README.md +1 -1
- package/dist/safe-bash/commands/playwright/index.d.ts.map +1 -1
- package/dist/safe-bash/commands/playwright/index.js +77 -4
- package/dist/safe-bash/commands/playwright/index.js.map +1 -1
- package/dist/safe-bash/playwright/abilities.d.ts +24 -5
- package/dist/safe-bash/playwright/abilities.d.ts.map +1 -1
- package/dist/safe-bash/playwright/abilities.js +9 -4
- package/dist/safe-bash/playwright/abilities.js.map +1 -1
- package/dist/safe-bash/playwright/ability-execution.d.ts +6 -1
- package/dist/safe-bash/playwright/ability-execution.d.ts.map +1 -1
- package/dist/safe-bash/playwright/ability-execution.js +77 -5
- package/dist/safe-bash/playwright/ability-execution.js.map +1 -1
- package/dist/safe-bash/playwright/adapter.d.ts +467 -11
- package/dist/safe-bash/playwright/adapter.d.ts.map +1 -1
- package/dist/safe-bash/playwright/adapter.js +213 -4
- package/dist/safe-bash/playwright/adapter.js.map +1 -1
- package/dist/safe-bash/playwright/builtins.d.ts +45 -5
- package/dist/safe-bash/playwright/builtins.d.ts.map +1 -1
- package/dist/safe-bash/playwright/builtins.js +15 -10
- package/dist/safe-bash/playwright/builtins.js.map +1 -1
- package/dist/safe-bash/playwright/canonical-locator-parser.d.ts +5 -0
- package/dist/safe-bash/playwright/canonical-locator-parser.d.ts.map +1 -0
- package/dist/safe-bash/playwright/canonical-locator-parser.js +146 -0
- package/dist/safe-bash/playwright/canonical-locator-parser.js.map +1 -0
- package/dist/safe-bash/playwright/capability-events.d.ts +15 -0
- package/dist/safe-bash/playwright/capability-events.d.ts.map +1 -0
- package/dist/safe-bash/playwright/capability-events.js +265 -0
- package/dist/safe-bash/playwright/capability-events.js.map +1 -0
- package/dist/safe-bash/playwright/capability-result.d.ts +33 -0
- package/dist/safe-bash/playwright/capability-result.d.ts.map +1 -0
- package/dist/safe-bash/playwright/capability-result.js +72 -0
- package/dist/safe-bash/playwright/capability-result.js.map +1 -0
- package/dist/safe-bash/playwright/config-ini.d.ts +3 -0
- package/dist/safe-bash/playwright/config-ini.d.ts.map +1 -0
- package/dist/safe-bash/playwright/config-ini.js +161 -0
- package/dist/safe-bash/playwright/config-ini.js.map +1 -0
- package/dist/safe-bash/playwright/controller.d.ts +39 -1
- package/dist/safe-bash/playwright/controller.d.ts.map +1 -1
- package/dist/safe-bash/playwright/controller.js +913 -77
- package/dist/safe-bash/playwright/controller.js.map +1 -1
- package/dist/safe-bash/playwright/devices.d.ts +229 -0
- package/dist/safe-bash/playwright/devices.d.ts.map +1 -0
- package/dist/safe-bash/playwright/devices.js +2740 -0
- package/dist/safe-bash/playwright/devices.js.map +1 -0
- package/dist/safe-bash/playwright/download-capabilities.d.ts +15 -0
- package/dist/safe-bash/playwright/download-capabilities.d.ts.map +1 -0
- package/dist/safe-bash/playwright/download-capabilities.js +161 -0
- package/dist/safe-bash/playwright/download-capabilities.js.map +1 -0
- package/dist/safe-bash/playwright/drop-capability.d.ts +3 -0
- package/dist/safe-bash/playwright/drop-capability.d.ts.map +1 -0
- package/dist/safe-bash/playwright/drop-capability.js +50 -0
- package/dist/safe-bash/playwright/drop-capability.js.map +1 -0
- package/dist/safe-bash/playwright/find.d.ts +8 -0
- package/dist/safe-bash/playwright/find.d.ts.map +1 -0
- package/dist/safe-bash/playwright/find.js +68 -0
- package/dist/safe-bash/playwright/find.js.map +1 -0
- package/dist/safe-bash/playwright/generated-locator.d.ts +4 -0
- package/dist/safe-bash/playwright/generated-locator.d.ts.map +1 -0
- package/dist/safe-bash/playwright/generated-locator.js +22 -0
- package/dist/safe-bash/playwright/generated-locator.js.map +1 -0
- package/dist/safe-bash/playwright/help.d.ts +4 -3
- package/dist/safe-bash/playwright/help.d.ts.map +1 -1
- package/dist/safe-bash/playwright/help.js +7 -52
- package/dist/safe-bash/playwright/help.js.map +1 -1
- package/dist/safe-bash/playwright/highlight.d.ts +8 -0
- package/dist/safe-bash/playwright/highlight.d.ts.map +1 -0
- package/dist/safe-bash/playwright/highlight.js +82 -0
- package/dist/safe-bash/playwright/highlight.js.map +1 -0
- package/dist/safe-bash/playwright/index.d.ts +5 -0
- package/dist/safe-bash/playwright/index.d.ts.map +1 -1
- package/dist/safe-bash/playwright/index.js +4 -0
- package/dist/safe-bash/playwright/index.js.map +1 -1
- package/dist/safe-bash/playwright/invocation.d.ts +24 -0
- package/dist/safe-bash/playwright/invocation.d.ts.map +1 -1
- package/dist/safe-bash/playwright/invocation.js +43 -14
- package/dist/safe-bash/playwright/invocation.js.map +1 -1
- package/dist/safe-bash/playwright/locator-selector.d.ts +5 -0
- package/dist/safe-bash/playwright/locator-selector.d.ts.map +1 -0
- package/dist/safe-bash/playwright/locator-selector.js +70 -0
- package/dist/safe-bash/playwright/locator-selector.js.map +1 -0
- package/dist/safe-bash/playwright/modal-capabilities.d.ts +20 -0
- package/dist/safe-bash/playwright/modal-capabilities.d.ts.map +1 -0
- package/dist/safe-bash/playwright/modal-capabilities.js +137 -0
- package/dist/safe-bash/playwright/modal-capabilities.js.map +1 -0
- package/dist/safe-bash/playwright/native-json-snapshot.d.ts +17 -0
- package/dist/safe-bash/playwright/native-json-snapshot.d.ts.map +1 -0
- package/dist/safe-bash/playwright/native-json-snapshot.js +94 -0
- package/dist/safe-bash/playwright/native-json-snapshot.js.map +1 -0
- package/dist/safe-bash/playwright/native-snapshot.d.ts +17 -0
- package/dist/safe-bash/playwright/native-snapshot.d.ts.map +1 -0
- package/dist/safe-bash/playwright/native-snapshot.js +164 -0
- package/dist/safe-bash/playwright/native-snapshot.js.map +1 -0
- package/dist/safe-bash/playwright/network-policy.d.ts +2 -0
- package/dist/safe-bash/playwright/network-policy.d.ts.map +1 -1
- package/dist/safe-bash/playwright/network-policy.js +1 -1
- package/dist/safe-bash/playwright/network-policy.js.map +1 -1
- package/dist/safe-bash/playwright/open-options.d.ts +14 -0
- package/dist/safe-bash/playwright/open-options.d.ts.map +1 -0
- package/dist/safe-bash/playwright/open-options.js +279 -0
- package/dist/safe-bash/playwright/open-options.js.map +1 -0
- package/dist/safe-bash/playwright/recording-capabilities.d.ts +6 -0
- package/dist/safe-bash/playwright/recording-capabilities.d.ts.map +1 -0
- package/dist/safe-bash/playwright/recording-capabilities.js +83 -0
- package/dist/safe-bash/playwright/recording-capabilities.js.map +1 -0
- package/dist/safe-bash/playwright/resource-limit.d.ts +6 -0
- package/dist/safe-bash/playwright/resource-limit.d.ts.map +1 -0
- package/dist/safe-bash/playwright/resource-limit.js +8 -0
- package/dist/safe-bash/playwright/resource-limit.js.map +1 -0
- package/dist/safe-bash/playwright/response.d.ts +22 -0
- package/dist/safe-bash/playwright/response.d.ts.map +1 -0
- package/dist/safe-bash/playwright/response.js +43 -0
- package/dist/safe-bash/playwright/response.js.map +1 -0
- package/dist/safe-bash/playwright/route-capabilities.d.ts +25 -0
- package/dist/safe-bash/playwright/route-capabilities.d.ts.map +1 -0
- package/dist/safe-bash/playwright/route-capabilities.js +138 -0
- package/dist/safe-bash/playwright/route-capabilities.js.map +1 -0
- package/dist/safe-bash/playwright/screenshot.d.ts +3 -1
- package/dist/safe-bash/playwright/screenshot.d.ts.map +1 -1
- package/dist/safe-bash/playwright/screenshot.js +15 -12
- package/dist/safe-bash/playwright/screenshot.js.map +1 -1
- package/dist/safe-bash/playwright/session-configuration.d.ts +35 -0
- package/dist/safe-bash/playwright/session-configuration.d.ts.map +1 -0
- package/dist/safe-bash/playwright/session-configuration.js +50 -0
- package/dist/safe-bash/playwright/session-configuration.js.map +1 -0
- package/dist/safe-bash/playwright/session-runtime.d.ts +8 -0
- package/dist/safe-bash/playwright/session-runtime.d.ts.map +1 -0
- package/dist/safe-bash/playwright/session-runtime.js +96 -0
- package/dist/safe-bash/playwright/session-runtime.js.map +1 -0
- package/dist/safe-bash/playwright/snapshot.d.ts +15 -2
- package/dist/safe-bash/playwright/snapshot.d.ts.map +1 -1
- package/dist/safe-bash/playwright/snapshot.js +88 -9
- package/dist/safe-bash/playwright/snapshot.js.map +1 -1
- package/dist/safe-bash/playwright/standard-capabilities.d.ts +4 -0
- package/dist/safe-bash/playwright/standard-capabilities.d.ts.map +1 -0
- package/dist/safe-bash/playwright/standard-capabilities.js +248 -0
- package/dist/safe-bash/playwright/standard-capabilities.js.map +1 -0
- package/dist/safe-bash/playwright/storage-capabilities.d.ts +4 -0
- package/dist/safe-bash/playwright/storage-capabilities.d.ts.map +1 -0
- package/dist/safe-bash/playwright/storage-capabilities.js +120 -0
- package/dist/safe-bash/playwright/storage-capabilities.js.map +1 -0
- package/dist/safe-bash/playwright/storage-state.d.ts +8 -0
- package/dist/safe-bash/playwright/storage-state.d.ts.map +1 -0
- package/dist/safe-bash/playwright/storage-state.js +122 -0
- package/dist/safe-bash/playwright/storage-state.js.map +1 -0
- package/dist/safe-bash/playwright/target-screenshot.d.ts +9 -0
- package/dist/safe-bash/playwright/target-screenshot.d.ts.map +1 -0
- package/dist/safe-bash/playwright/target-screenshot.js +28 -0
- package/dist/safe-bash/playwright/target-screenshot.js.map +1 -0
- package/dist/safe-bash/playwright/targets.d.ts +12 -0
- package/dist/safe-bash/playwright/targets.d.ts.map +1 -0
- package/dist/safe-bash/playwright/targets.js +36 -0
- package/dist/safe-bash/playwright/targets.js.map +1 -0
- package/dist/safe-bash/playwright/tracing-capabilities.d.ts +12 -0
- package/dist/safe-bash/playwright/tracing-capabilities.d.ts.map +1 -0
- package/dist/safe-bash/playwright/tracing-capabilities.js +100 -0
- package/dist/safe-bash/playwright/tracing-capabilities.js.map +1 -0
- package/dist/safe-bash/playwright/webmcp-capabilities.d.ts +6 -0
- package/dist/safe-bash/playwright/webmcp-capabilities.d.ts.map +1 -0
- package/dist/safe-bash/playwright/webmcp-capabilities.js +122 -0
- package/dist/safe-bash/playwright/webmcp-capabilities.js.map +1 -0
- package/dist/safe-bash/playwright/workspace-assets.d.ts +5 -0
- package/dist/safe-bash/playwright/workspace-assets.d.ts.map +1 -0
- package/dist/safe-bash/playwright/workspace-assets.js +17 -0
- package/dist/safe-bash/playwright/workspace-assets.js.map +1 -0
- package/dist/safe-bash/playwright/workspace.d.ts +3 -0
- package/dist/safe-bash/playwright/workspace.d.ts.map +1 -0
- package/dist/safe-bash/playwright/workspace.js +48 -0
- package/dist/safe-bash/playwright/workspace.js.map +1 -0
- package/package.json +4 -3
- package/third-party/playwright/LICENSE +208 -0
- package/third-party/playwright/NOTICE +14 -0
|
@@ -4,19 +4,45 @@ import { parseInvocation, validatePlaywrightSessionName } from './invocation.js'
|
|
|
4
4
|
import { formatPlaywrightHelp } from './help.js';
|
|
5
5
|
import { registerPlaywrightAbilities } from './abilities.js';
|
|
6
6
|
import { executePlaywrightAbility } from './ability-execution.js';
|
|
7
|
+
import { playwrightCliCompatibilityVersion, playwrightCodeString, serializePlaywrightResult } from './response.js';
|
|
8
|
+
import { capabilityArtifactName } from './capability-result.js';
|
|
9
|
+
import { isPlaywrightSnapshotRef, resolvePlaywrightTarget } from './targets.js';
|
|
10
|
+
import { capturePlaywrightTargetScreenshot } from './target-screenshot.js';
|
|
11
|
+
import { flushPlaywrightConsole, observePlaywrightCapabilities } from './capability-events.js';
|
|
12
|
+
import { getPlaywrightModal, observePlaywrightModals, onPlaywrightModal } from './modal-capabilities.js';
|
|
13
|
+
import { findPlaywrightSnapshot } from './find.js';
|
|
14
|
+
import { collectPlaywrightDownloads, observePlaywrightDownloads } from './download-capabilities.js';
|
|
15
|
+
import { generatedPlaywrightLocator } from './generated-locator.js';
|
|
16
|
+
import { capturePlaywrightRoutes } from './route-capabilities.js';
|
|
17
|
+
import { updatePlaywrightHighlight } from './highlight.js';
|
|
18
|
+
import { parsePlaywrightContextOptions, resolvePlaywrightOpenOptions } from './open-options.js';
|
|
19
|
+
import { playwrightLocatorSelector, setPlaywrightTestIdAttribute } from './locator-selector.js';
|
|
20
|
+
import { initializePlaywrightWorkspace } from './workspace.js';
|
|
21
|
+
import { parsePlaywrightSessionConfiguration } from './session-configuration.js';
|
|
22
|
+
import { installPlaywrightConfiguredNetwork, playwrightInitPageSource } from './session-runtime.js';
|
|
23
|
+
import { flushPlaywrightTrace, preparePlaywrightTraceRelease } from './tracing-capabilities.js';
|
|
24
|
+
import { resolvePath } from '../contracts/path.js';
|
|
7
25
|
export function createPlaywrightController(options = {}) {
|
|
8
26
|
if (!options || typeof options !== 'object')
|
|
9
27
|
throw new TypeError('Invalid Playwright configuration');
|
|
10
28
|
if (options.adapter !== undefined && (!options.adapter || typeof options.adapter.acquire !== 'function'))
|
|
11
29
|
throw new TypeError('An injected Playwright adapter is required');
|
|
12
|
-
if (Object.keys(options).some(key => !['adapter', 'abilities', 'limits', 'billing'].includes(key)))
|
|
30
|
+
if (Object.keys(options).some(key => !['adapter', 'abilities', 'limits', 'billing', 'persistence'].includes(key)))
|
|
13
31
|
throw new TypeError('Unsupported Playwright configuration');
|
|
14
|
-
if (options.
|
|
32
|
+
if (options.persistence && ['restore', 'checkpoint', 'delete'].some(key => typeof Reflect.get(options.persistence, key) !== 'function'))
|
|
33
|
+
throw new TypeError('Invalid Playwright persistence');
|
|
34
|
+
if (options.persistence?.close !== undefined && typeof options.persistence.close !== 'function')
|
|
35
|
+
throw new TypeError('Invalid Playwright persistence close');
|
|
36
|
+
if (options.persistence?.list !== undefined && typeof options.persistence.list !== 'function')
|
|
37
|
+
throw new TypeError('Invalid Playwright persistence list');
|
|
38
|
+
if (options.limits !== undefined && (!options.limits || typeof options.limits !== 'object' || Object.keys(options.limits).some(key => !['maxSessions', 'actionTimeoutMs', 'codeExecutionTimeoutMs', 'maxSnapshotBytes', 'maxSnapshotRefs', 'maxArtifactBytes', 'maxTabs', 'maxCommandBytes'].includes(key))))
|
|
15
39
|
throw new TypeError('Unsupported Playwright limits');
|
|
16
40
|
if (options.billing !== undefined)
|
|
17
41
|
throw new Error('Live billing is not implemented');
|
|
18
42
|
const maxSessions = options.limits?.maxSessions ?? 4;
|
|
19
|
-
const actionTimeoutMs = options.limits?.actionTimeoutMs ??
|
|
43
|
+
const actionTimeoutMs = options.limits?.actionTimeoutMs ?? 5_000;
|
|
44
|
+
// Isolated startup and multiple native actions share this host budget, not one action's timeout.
|
|
45
|
+
const codeExecutionTimeoutMs = options.limits?.codeExecutionTimeoutMs ?? 30_000;
|
|
20
46
|
const maxSnapshotBytes = options.limits?.maxSnapshotBytes ?? 256 * 1024;
|
|
21
47
|
const maxSnapshotRefs = options.limits?.maxSnapshotRefs ?? 1000;
|
|
22
48
|
const maxArtifactBytes = options.limits?.maxArtifactBytes ?? 16 * 1024 * 1024;
|
|
@@ -24,30 +50,86 @@ export function createPlaywrightController(options = {}) {
|
|
|
24
50
|
const maxCommandBytes = options.limits?.maxCommandBytes ?? 16 * 1024 * 1024;
|
|
25
51
|
const abilities = registerPlaywrightAbilities(options.abilities, options.adapter !== undefined);
|
|
26
52
|
let refSequence = 0;
|
|
27
|
-
for (const value of [maxSessions, actionTimeoutMs, maxSnapshotBytes, maxSnapshotRefs, maxArtifactBytes, maxTabs, maxCommandBytes])
|
|
53
|
+
for (const value of [maxSessions, actionTimeoutMs, codeExecutionTimeoutMs, maxSnapshotBytes, maxSnapshotRefs, maxArtifactBytes, maxTabs, maxCommandBytes])
|
|
28
54
|
if (!Number.isSafeInteger(value) || value < 1)
|
|
29
55
|
throw new RangeError('Invalid Playwright limit');
|
|
30
56
|
const sessions = new Map();
|
|
57
|
+
const explicitlyClosed = new Set();
|
|
58
|
+
let suppressUnknownRestores = false;
|
|
31
59
|
const tails = new Map();
|
|
32
60
|
const work = new Set();
|
|
33
61
|
const lifetime = new AbortController();
|
|
34
62
|
let generation = 0;
|
|
35
63
|
let disposal;
|
|
64
|
+
const sessionActionTimeout = (session) => session?.configuration?.timeouts?.action ?? actionTimeoutMs;
|
|
65
|
+
const sessionNavigationTimeout = (session) => session?.configuration?.timeouts?.navigation ?? options.limits?.actionTimeoutMs ?? 60_000;
|
|
66
|
+
const initializeContext = async (session) => {
|
|
67
|
+
if (session.configuration?.codegen && !['typescript', 'none'].includes(session.configuration.codegen) && !session.lease?.generateActionCode)
|
|
68
|
+
throw new Error('Configured codegen language requires native action generation');
|
|
69
|
+
await installPlaywrightConfiguredNetwork(session.lease.context, session.configuration);
|
|
70
|
+
const configured = session.lease.context;
|
|
71
|
+
configured.setDefaultTimeout?.(sessionActionTimeout(session));
|
|
72
|
+
configured.setDefaultNavigationTimeout?.(sessionNavigationTimeout(session));
|
|
73
|
+
const scripts = session.configuration?.initScripts;
|
|
74
|
+
if (!scripts?.length)
|
|
75
|
+
return;
|
|
76
|
+
const context = session.lease.context;
|
|
77
|
+
if (!context.addInitScript)
|
|
78
|
+
throw new Error('Browser context init scripts unsupported');
|
|
79
|
+
// One native registration preserves source order across every future page.
|
|
80
|
+
await context.addInitScript({ content: scripts.join('\n;\n') });
|
|
81
|
+
};
|
|
82
|
+
const savedSessions = async (signal) => {
|
|
83
|
+
const entries = await options.persistence?.list?.(signal) ?? [];
|
|
84
|
+
signal.throwIfAborted();
|
|
85
|
+
if (!Array.isArray(entries) || entries.length > maxSnapshotRefs)
|
|
86
|
+
throw new PlaywrightResourceLimitError('Playwright session list limit exceeded');
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (!entry || typeof entry !== 'object')
|
|
89
|
+
throw new Error('Invalid persisted session');
|
|
90
|
+
validatePlaywrightSessionName(entry.name);
|
|
91
|
+
if (entry.expiresAt !== undefined && (!Number.isSafeInteger(entry.expiresAt) || entry.expiresAt < 0))
|
|
92
|
+
throw new Error('Invalid persisted session expiry');
|
|
93
|
+
}
|
|
94
|
+
return entries.filter(entry => !explicitlyClosed.has(entry.name) && (!suppressUnknownRestores || sessions.get(entry.name)?.state === 'open') && (entry.expiresAt === undefined || entry.expiresAt > Date.now()));
|
|
95
|
+
};
|
|
96
|
+
const checkpoint = async (session, signal, activity = true) => {
|
|
97
|
+
if (activity && session.idleTimeoutMs)
|
|
98
|
+
session.expiresAt = Date.now() + session.idleTimeoutMs;
|
|
99
|
+
scheduleExpiry(session);
|
|
100
|
+
if (!options.persistence || session.state !== 'open' || !session.lease || session.failure)
|
|
101
|
+
return;
|
|
102
|
+
if (session.lease.context.pages().some(page => getPlaywrightModal(page)))
|
|
103
|
+
return;
|
|
104
|
+
signal.throwIfAborted();
|
|
105
|
+
await options.persistence.checkpoint({ name: session.name, context: session.lease.context,
|
|
106
|
+
...(session.page ? { selectedPage: session.page } : {}), ...(session.expiresAt === undefined ? {} : { expiresAt: session.expiresAt }),
|
|
107
|
+
...(session.idleTimeoutMs === undefined ? {} : { idleTimeoutMs: session.idleTimeoutMs }),
|
|
108
|
+
...(session.contextOptions === undefined ? {} : { contextOptions: structuredClone(session.contextOptions) }),
|
|
109
|
+
...(session.configuration === undefined ? {} : { configuration: structuredClone(session.configuration) }) }, signal);
|
|
110
|
+
signal.throwIfAborted();
|
|
111
|
+
};
|
|
36
112
|
const release = (session) => {
|
|
37
113
|
if (session.releasing)
|
|
38
114
|
return session.releasing;
|
|
39
115
|
session.state = 'closing';
|
|
116
|
+
clearTimeout(session.expiryTimer);
|
|
40
117
|
session.releasing = Promise.resolve().then(async () => {
|
|
41
118
|
try {
|
|
119
|
+
// Native tracing must flush while the context is still alive. Arbitrary
|
|
120
|
+
// cleanup remains concurrent with lease closure to unblock browser work.
|
|
121
|
+
const traceResults = session.lease ? await Promise.allSettled([preparePlaywrightTraceRelease(session.lease.context)]) : [];
|
|
42
122
|
const callbacks = [...session.cleanups];
|
|
43
123
|
session.cleanups.clear();
|
|
44
124
|
const custom = callbacks.map(cleanup => Promise.resolve().then(cleanup));
|
|
45
125
|
const results = await Promise.allSettled([
|
|
46
|
-
Promise.resolve().then(() => session.detachPage?.()), ...custom,
|
|
126
|
+
Promise.resolve().then(() => session.detachPage?.()), Promise.resolve().then(() => session.detachContext?.()), Promise.resolve().then(() => session.disposeCapabilities?.()), ...custom,
|
|
47
127
|
session.snapshot.invalidate(true), Promise.resolve().then(() => session.unsubscribe?.()),
|
|
48
128
|
Promise.resolve().then(() => session.lease?.release()),
|
|
129
|
+
...[...session.pendingActions ?? []].map(action => action.catch(() => { })),
|
|
130
|
+
...[...session.pendingInitializations ?? []].map(action => action.catch(() => { })),
|
|
49
131
|
]);
|
|
50
|
-
const errors = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
|
|
132
|
+
const errors = [...traceResults, ...results].flatMap(result => result.status === 'rejected' ? [result.reason] : []);
|
|
51
133
|
if (errors.length === 1)
|
|
52
134
|
throw errors[0];
|
|
53
135
|
if (errors.length > 1)
|
|
@@ -62,6 +144,25 @@ export function createPlaywrightController(options = {}) {
|
|
|
62
144
|
void session.releasing.catch(() => { });
|
|
63
145
|
return session.releasing;
|
|
64
146
|
};
|
|
147
|
+
const checkpointAndRelease = async (session, signal) => {
|
|
148
|
+
const errors = [];
|
|
149
|
+
try {
|
|
150
|
+
await checkpoint(session, signal, false);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
errors.push(error);
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
await release(session);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
errors.push(error);
|
|
160
|
+
}
|
|
161
|
+
if (errors.length === 1)
|
|
162
|
+
throw errors[0];
|
|
163
|
+
if (errors.length)
|
|
164
|
+
throw new AggregateError(errors, 'Playwright checkpoint and retirement failed');
|
|
165
|
+
};
|
|
65
166
|
const enqueue = (name, task) => {
|
|
66
167
|
const operation = (tails.get(name) ?? Promise.resolve()).then(task);
|
|
67
168
|
const tail = operation.then(() => { }, () => { });
|
|
@@ -73,10 +174,12 @@ export function createPlaywrightController(options = {}) {
|
|
|
73
174
|
const enforceTabLimit = (session) => {
|
|
74
175
|
if (!session.lease || session.releasing || session.lease.context.pages().length <= maxTabs)
|
|
75
176
|
return;
|
|
76
|
-
session.failure = new
|
|
177
|
+
session.failure = new PlaywrightResourceLimitError('Playwright tab limit exceeded');
|
|
77
178
|
void release(session).catch(() => { });
|
|
78
179
|
};
|
|
79
180
|
const selectPage = async (session, page, check) => {
|
|
181
|
+
check();
|
|
182
|
+
await initializePage(session, page);
|
|
80
183
|
check();
|
|
81
184
|
session.detachPage?.();
|
|
82
185
|
await session.snapshot.invalidate();
|
|
@@ -91,6 +194,50 @@ export function createPlaywrightController(options = {}) {
|
|
|
91
194
|
page.on('close', closed);
|
|
92
195
|
}
|
|
93
196
|
};
|
|
197
|
+
const initializePage = async (session, page) => {
|
|
198
|
+
const key = page.mainFrame?.() ?? page;
|
|
199
|
+
setPlaywrightTestIdAttribute(page, session.configuration?.testIdAttribute);
|
|
200
|
+
session.pageInitializations ??= new WeakMap();
|
|
201
|
+
let operation = session.pageInitializations.get(key);
|
|
202
|
+
if (!operation) {
|
|
203
|
+
operation = (async () => {
|
|
204
|
+
for (const module of session.configuration?.initPages ?? []) {
|
|
205
|
+
if (!session.lease?.executeCode)
|
|
206
|
+
throw new Error('Browser initPage requires isolated native code execution');
|
|
207
|
+
try {
|
|
208
|
+
await session.lease.executeCode({ page, source: playwrightInitPageSource(module.source), signal: lifetime.signal,
|
|
209
|
+
timeoutMs: codeExecutionTimeoutMs, maxOutputBytes: maxCommandBytes, maxPages: maxTabs });
|
|
210
|
+
}
|
|
211
|
+
catch (cause) {
|
|
212
|
+
throw new Error(`Failed to load init page "${module.filename}": ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
session.pageInitializations.set(key, operation);
|
|
217
|
+
(session.pendingInitializations ??= new Set()).add(operation);
|
|
218
|
+
void operation.then(() => { session.pendingInitializations.delete(operation); }, error => { session.pendingInitializations.delete(operation); session.failure = error instanceof Error ? error : new Error(String(error)); });
|
|
219
|
+
}
|
|
220
|
+
await operation;
|
|
221
|
+
};
|
|
222
|
+
const observeContext = (session) => {
|
|
223
|
+
session.detachContext?.();
|
|
224
|
+
const context = session.lease.context;
|
|
225
|
+
const onPage = (page) => { enforceTabLimit(session); void initializePage(session, page).catch(() => { }); };
|
|
226
|
+
session.detachContext = () => { context.off('page', onPage); };
|
|
227
|
+
const cleanups = [];
|
|
228
|
+
session.disposeCapabilities = async () => {
|
|
229
|
+
const results = await Promise.allSettled(cleanups.map(cleanup => Promise.resolve().then(cleanup)));
|
|
230
|
+
const errors = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
|
|
231
|
+
if (errors.length === 1)
|
|
232
|
+
throw errors[0];
|
|
233
|
+
if (errors.length)
|
|
234
|
+
throw new AggregateError(errors, 'Browser observation cleanup failed');
|
|
235
|
+
};
|
|
236
|
+
observePlaywrightCapabilities(context, cleanup => cleanups.push(cleanup), { maxCommandBytes, maxArtifactBytes });
|
|
237
|
+
observePlaywrightModals(context, cleanup => cleanups.push(cleanup));
|
|
238
|
+
observePlaywrightDownloads(context, cleanup => cleanups.push(cleanup), { maxMetadataBytes: maxCommandBytes });
|
|
239
|
+
context.on('page', onPage);
|
|
240
|
+
};
|
|
94
241
|
const observeSession = (session) => {
|
|
95
242
|
const unsubscribe = session.lease.onClosed(() => {
|
|
96
243
|
if (sessions.get(session.name) !== session || session.state === 'closed')
|
|
@@ -104,17 +251,15 @@ export function createPlaywrightController(options = {}) {
|
|
|
104
251
|
unsubscribe();
|
|
105
252
|
throw new Error(`Session closed: ${session.name}`);
|
|
106
253
|
}
|
|
107
|
-
|
|
108
|
-
const onPage = () => enforceTabLimit(session);
|
|
109
|
-
session.cleanups.add(async () => { context.off('page', onPage); });
|
|
110
|
-
context.on('page', onPage);
|
|
254
|
+
observeContext(session);
|
|
111
255
|
};
|
|
112
256
|
const retireExpired = () => {
|
|
113
257
|
const now = Date.now();
|
|
114
|
-
const expired = [...sessions.values()].filter(session => session.state === 'open' && session.expiresAt !== undefined && session.expiresAt <= now);
|
|
258
|
+
const expired = [...sessions.values()].filter(session => session.state === 'open' && !session.idlePaused && session.expiresAt !== undefined && session.expiresAt <= now);
|
|
115
259
|
if (!expired.length)
|
|
116
260
|
return undefined;
|
|
117
261
|
return Promise.allSettled(expired.map(session => {
|
|
262
|
+
explicitlyClosed.add(session.name);
|
|
118
263
|
session.failure = new Error(`Session expired: ${session.name}; reopen explicitly`);
|
|
119
264
|
return release(session);
|
|
120
265
|
})).then(results => {
|
|
@@ -123,18 +268,36 @@ export function createPlaywrightController(options = {}) {
|
|
|
123
268
|
throw new AggregateError(errors, 'Playwright expiry cleanup failed');
|
|
124
269
|
});
|
|
125
270
|
};
|
|
271
|
+
const scheduleExpiry = (session) => {
|
|
272
|
+
clearTimeout(session.expiryTimer);
|
|
273
|
+
if (session.expiresAt === undefined || session.state !== 'open')
|
|
274
|
+
return;
|
|
275
|
+
session.expiryTimer = setTimeout(() => {
|
|
276
|
+
void enqueue(session.name, async () => {
|
|
277
|
+
await retireExpired();
|
|
278
|
+
if (session.state === 'open')
|
|
279
|
+
scheduleExpiry(session);
|
|
280
|
+
}).catch(() => { });
|
|
281
|
+
}, Math.min(2_147_483_647, Math.max(0, session.expiresAt - Date.now())));
|
|
282
|
+
session.expiryTimer.unref?.();
|
|
283
|
+
};
|
|
126
284
|
const inspectSessions = () => Object.freeze([...sessions.values()]
|
|
127
285
|
.filter(session => session.state === 'open' && (session.expiresAt === undefined || session.expiresAt > Date.now()))
|
|
128
286
|
.map(session => Object.freeze({ name: session.name, context: session.lease.context,
|
|
129
287
|
...(session.page === undefined ? {} : { selectedPage: session.page }),
|
|
130
|
-
...(session.expiresAt === undefined ? {} : { expiresAt: session.expiresAt }),
|
|
131
|
-
|
|
132
|
-
|
|
288
|
+
...(session.expiresAt === undefined ? {} : { expiresAt: session.expiresAt }),
|
|
289
|
+
...(session.idleTimeoutMs === undefined ? {} : { idleTimeoutMs: session.idleTimeoutMs }),
|
|
290
|
+
...(session.contextOptions === undefined ? {} : { contextOptions: structuredClone(session.contextOptions) }),
|
|
291
|
+
...(session.configuration === undefined ? {} : { configuration: structuredClone(session.configuration) }), })));
|
|
292
|
+
const restoreSession = async (request, withinQueue = false) => {
|
|
293
|
+
if (!request || typeof request !== 'object' || Object.keys(request).some(key => !['name', 'expiresAt', 'idleTimeoutMs', 'contextOptions', 'configuration', 'signal', 'acquire'].includes(key))
|
|
133
294
|
|| typeof request.acquire !== 'function' || request.signal !== undefined && typeof request.signal.throwIfAborted !== 'function')
|
|
134
295
|
throw new TypeError('Invalid Playwright session restoration');
|
|
135
296
|
validatePlaywrightSessionName(request.name);
|
|
136
297
|
if (request.expiresAt !== undefined && (!Number.isSafeInteger(request.expiresAt) || request.expiresAt < 0))
|
|
137
298
|
throw new TypeError('Invalid Playwright session expiry');
|
|
299
|
+
if (request.idleTimeoutMs !== undefined && (!Number.isSafeInteger(request.idleTimeoutMs) || request.idleTimeoutMs < 0))
|
|
300
|
+
throw new TypeError('Invalid Playwright idle timeout');
|
|
138
301
|
const { name, expiresAt, acquire } = request;
|
|
139
302
|
const signal = request.signal ? AbortSignal.any([request.signal, lifetime.signal]) : lifetime.signal;
|
|
140
303
|
const check = () => {
|
|
@@ -143,7 +306,7 @@ export function createPlaywrightController(options = {}) {
|
|
|
143
306
|
throw new Error(`Session expired: ${name}; reopen explicitly`);
|
|
144
307
|
};
|
|
145
308
|
check();
|
|
146
|
-
const
|
|
309
|
+
const restore = async () => {
|
|
147
310
|
check();
|
|
148
311
|
await retireExpired();
|
|
149
312
|
const previous = sessions.get(name);
|
|
@@ -153,13 +316,18 @@ export function createPlaywrightController(options = {}) {
|
|
|
153
316
|
await previous.releasing;
|
|
154
317
|
check();
|
|
155
318
|
if ([...sessions.values()].filter(session => session.state !== 'closed').length >= maxSessions)
|
|
156
|
-
throw new
|
|
319
|
+
throw new PlaywrightResourceLimitError('Playwright session capacity exceeded');
|
|
157
320
|
const epoch = Array.from(crypto.getRandomValues(new Uint32Array(4)), value => String(value).padStart(10, '0')).join('');
|
|
158
321
|
const session = { name, generation: ++generation, state: 'acquiring', cleanups: new Set(),
|
|
159
322
|
snapshot: createSnapshotEngine({ maxSnapshotBytes, maxSnapshotRefs }, () => `e${epoch}${++refSequence}`),
|
|
160
323
|
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
324
|
+
...(request.idleTimeoutMs === undefined ? {} : { idleTimeoutMs: request.idleTimeoutMs }),
|
|
325
|
+
...(request.contextOptions === undefined ? {} : { contextOptions: parsePlaywrightContextOptions(request.contextOptions, maxArtifactBytes) }),
|
|
326
|
+
...(request.configuration === undefined ? {} : { configuration: parsePlaywrightSessionConfiguration(request.configuration, maxArtifactBytes) }),
|
|
161
327
|
};
|
|
162
328
|
sessions.set(name, session);
|
|
329
|
+
const cancel = () => { if (session.lease)
|
|
330
|
+
void release(session).catch(() => { }); };
|
|
163
331
|
try {
|
|
164
332
|
const restored = await acquire.call(request, { signal });
|
|
165
333
|
session.lease = restored.lease;
|
|
@@ -167,25 +335,34 @@ export function createPlaywrightController(options = {}) {
|
|
|
167
335
|
if (!session.lease || typeof session.lease.release !== 'function' || typeof session.lease.onClosed !== 'function'
|
|
168
336
|
|| !session.lease.context || ['pages', 'newPage', 'close', 'on', 'off'].some(key => typeof Reflect.get(session.lease.context, key) !== 'function'))
|
|
169
337
|
throw new TypeError('Invalid restored Playwright lease');
|
|
338
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
170
339
|
observeSession(session);
|
|
340
|
+
await initializeContext(session);
|
|
171
341
|
const pages = [...session.lease.context.pages()];
|
|
172
342
|
if (pages.length > maxTabs)
|
|
173
|
-
throw new
|
|
343
|
+
throw new PlaywrightResourceLimitError('Playwright tab limit exceeded');
|
|
174
344
|
if (restored.selectedPage !== undefined) {
|
|
175
345
|
if (!pages.includes(restored.selectedPage))
|
|
176
346
|
throw new Error('Selected tab does not belong to the restored context');
|
|
177
347
|
await selectPage(session, restored.selectedPage, check);
|
|
178
348
|
}
|
|
179
349
|
check();
|
|
350
|
+
if (restored.initialize !== undefined) {
|
|
351
|
+
if (typeof restored.initialize !== 'function')
|
|
352
|
+
throw new TypeError('Invalid Playwright session initializer');
|
|
353
|
+
await restored.initialize({ signal });
|
|
354
|
+
check();
|
|
355
|
+
}
|
|
180
356
|
if (session.releasing)
|
|
181
357
|
throw new Error(`Session closed: ${name}`);
|
|
182
358
|
const currentPages = [...session.lease.context.pages()];
|
|
183
359
|
if (currentPages.length > maxTabs)
|
|
184
|
-
throw new
|
|
360
|
+
throw new PlaywrightResourceLimitError('Playwright tab limit exceeded');
|
|
185
361
|
if (restored.selectedPage !== undefined && !currentPages.includes(restored.selectedPage))
|
|
186
362
|
throw new Error('Selected tab closed during restoration');
|
|
187
363
|
session.pages = currentPages;
|
|
188
364
|
session.state = 'open';
|
|
365
|
+
scheduleExpiry(session);
|
|
189
366
|
}
|
|
190
367
|
catch (error) {
|
|
191
368
|
const reason = signal.aborted ? signal.reason : error;
|
|
@@ -197,7 +374,11 @@ export function createPlaywrightController(options = {}) {
|
|
|
197
374
|
}
|
|
198
375
|
throw reason;
|
|
199
376
|
}
|
|
200
|
-
|
|
377
|
+
finally {
|
|
378
|
+
signal.removeEventListener('abort', cancel);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
const operation = withinQueue ? restore() : enqueue(name, restore);
|
|
201
382
|
work.add(operation);
|
|
202
383
|
try {
|
|
203
384
|
await operation;
|
|
@@ -207,23 +388,41 @@ export function createPlaywrightController(options = {}) {
|
|
|
207
388
|
}
|
|
208
389
|
};
|
|
209
390
|
const run = async (invocation) => {
|
|
391
|
+
const writtenFiles = new Set();
|
|
392
|
+
const original = invocation;
|
|
393
|
+
if (original.writeArtifact)
|
|
394
|
+
invocation = { ...original, async writeArtifact(bytes, filename) {
|
|
395
|
+
await original.writeArtifact(bytes, filename);
|
|
396
|
+
if (filename !== undefined)
|
|
397
|
+
writtenFiles.add(resolvePath(original.workspace?.cwd ?? '/', filename));
|
|
398
|
+
} };
|
|
210
399
|
if (lifetime.signal.aborted)
|
|
211
400
|
throw new Error('Playwright controller is disposed');
|
|
212
401
|
const parsed = parseInvocation(invocation, abilities, options.adapter);
|
|
213
402
|
invocation.signal.throwIfAborted();
|
|
214
403
|
const local = new AbortController();
|
|
215
404
|
let active;
|
|
405
|
+
let traceFlushed = false;
|
|
216
406
|
let retained = false;
|
|
217
407
|
let finished = false;
|
|
218
408
|
let cleanupCompletion;
|
|
409
|
+
const ownedTargets = new Set();
|
|
410
|
+
const targetLocators = new Map();
|
|
219
411
|
const cleanup = () => {
|
|
220
412
|
if (cleanupCompletion)
|
|
221
413
|
return cleanupCompletion;
|
|
222
414
|
// Install completion before abort dispatch can reenter cleanup.
|
|
223
415
|
cleanupCompletion = Promise.resolve().then(async () => {
|
|
224
416
|
await operation.catch(() => { });
|
|
417
|
+
const retirements = [...ownedTargets].map(handle => handle.dispose());
|
|
225
418
|
if (active && (!retained || active.releasing))
|
|
226
|
-
|
|
419
|
+
retirements.push(release(active));
|
|
420
|
+
const settled = await Promise.allSettled(retirements);
|
|
421
|
+
const errors = settled.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
|
|
422
|
+
if (errors.length === 1)
|
|
423
|
+
throw errors[0];
|
|
424
|
+
if (errors.length)
|
|
425
|
+
throw new AggregateError(errors, 'Playwright invocation cleanup failed');
|
|
227
426
|
});
|
|
228
427
|
void cleanupCompletion.catch(() => { });
|
|
229
428
|
// Close admission immediately and unblock cooperative browser work before
|
|
@@ -250,9 +449,171 @@ export function createPlaywrightController(options = {}) {
|
|
|
250
449
|
local.signal.throwIfAborted();
|
|
251
450
|
lifetime.signal.throwIfAborted();
|
|
252
451
|
};
|
|
452
|
+
const writeResult = async (result) => {
|
|
453
|
+
check();
|
|
454
|
+
const text = serializePlaywrightResult(configuredResult(result), parsed);
|
|
455
|
+
if (text.length > maxCommandBytes || new TextEncoder().encode(text).byteLength > maxCommandBytes)
|
|
456
|
+
throw new PlaywrightResourceLimitError('Playwright command byte limit exceeded');
|
|
457
|
+
await invocation.write(text);
|
|
458
|
+
check();
|
|
459
|
+
};
|
|
460
|
+
const configuredResult = (result) => {
|
|
461
|
+
const language = active?.configuration?.codegen ?? 'typescript';
|
|
462
|
+
return { sections: result.sections.flatMap(section => section.title !== 'Ran Playwright code' ? [section] : language === 'none' ? [] : [{ ...section, codeframe: language === 'typescript' ? 'js' : language }]) };
|
|
463
|
+
};
|
|
464
|
+
const actionCode = (session, action, fallback) => {
|
|
465
|
+
const language = session.configuration?.codegen ?? 'typescript';
|
|
466
|
+
if (language === 'none')
|
|
467
|
+
return '';
|
|
468
|
+
if (!session.lease?.generateActionCode) {
|
|
469
|
+
if (language !== 'typescript')
|
|
470
|
+
throw new Error('Configured codegen language requires native action generation');
|
|
471
|
+
return fallback;
|
|
472
|
+
}
|
|
473
|
+
const code = session.lease.generateActionCode({ language, action });
|
|
474
|
+
if (typeof code !== 'string' || new TextEncoder().encode(code).byteLength > maxCommandBytes)
|
|
475
|
+
throw new PlaywrightResourceLimitError('Playwright generated code byte limit exceeded');
|
|
476
|
+
return code;
|
|
477
|
+
};
|
|
478
|
+
const enforceOutputBudget = async () => {
|
|
479
|
+
const limit = active?.configuration?.outputMaxSize;
|
|
480
|
+
if (!limit)
|
|
481
|
+
return;
|
|
482
|
+
const workspace = invocation.workspace;
|
|
483
|
+
if (!workspace?.listFiles || !workspace.removeFile)
|
|
484
|
+
throw new Error('Playwright outputMaxSize requires virtual workspace retention support');
|
|
485
|
+
const directory = resolvePath(workspace.cwd, active?.configuration?.outputDir ?? '.playwright-cli');
|
|
486
|
+
const files = await workspace.listFiles(directory, 4096);
|
|
487
|
+
check();
|
|
488
|
+
if (!Array.isArray(files) || files.length > 4096)
|
|
489
|
+
throw new PlaywrightResourceLimitError('Playwright output directory entry limit exceeded');
|
|
490
|
+
const unique = new Set();
|
|
491
|
+
let total = 0;
|
|
492
|
+
for (const entry of files) {
|
|
493
|
+
const name = resolvePath(workspace.cwd, entry.filename);
|
|
494
|
+
if (!name.startsWith(directory.endsWith('/') ? directory : directory + '/') || unique.has(name) || !Number.isSafeInteger(entry.size) || entry.size < 0 || !Number.isFinite(entry.mtimeMs))
|
|
495
|
+
throw new Error('Invalid Playwright output file metadata');
|
|
496
|
+
unique.add(name);
|
|
497
|
+
total += entry.size;
|
|
498
|
+
}
|
|
499
|
+
for (const entry of [...files].sort((a, b) => a.mtimeMs - b.mtimeMs)) {
|
|
500
|
+
if (total <= limit)
|
|
501
|
+
break;
|
|
502
|
+
const filename = resolvePath(workspace.cwd, entry.filename);
|
|
503
|
+
if (writtenFiles.has(filename))
|
|
504
|
+
continue;
|
|
505
|
+
await workspace.removeFile(filename);
|
|
506
|
+
check();
|
|
507
|
+
total -= entry.size;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
const downloadSections = async (session) => {
|
|
511
|
+
if (!session.page)
|
|
512
|
+
return [];
|
|
513
|
+
const downloads = await collectPlaywrightDownloads(session.page, session.lease?.captureArtifact, { signal: local.signal, maxBytes: Math.min(maxArtifactBytes, maxCommandBytes), ...(session.configuration ? { configuration: session.configuration } : {}) }, session.lease?.captureDownload);
|
|
514
|
+
if (!downloads.length)
|
|
515
|
+
return [];
|
|
516
|
+
if (!invocation.writeArtifact)
|
|
517
|
+
throw new Error('Artifact byte destination unsupported');
|
|
518
|
+
for (const download of downloads)
|
|
519
|
+
await invocation.writeArtifact(download.bytes, download.filename);
|
|
520
|
+
return [{ title: 'Events', content: downloads.map(download => `- Downloaded file [${download.filename.split('/').at(-1)}](${download.filename})`).join('\n') }];
|
|
521
|
+
};
|
|
522
|
+
const flushTrace = async (session) => {
|
|
523
|
+
if (!session.lease?.captureTrace)
|
|
524
|
+
return [];
|
|
525
|
+
return flushPlaywrightTrace(session.lease.context, session.lease.captureTrace, {
|
|
526
|
+
signal: local.signal, maxBytes: Math.min(maxArtifactBytes, maxCommandBytes), async writeArtifact(bytes, filename) {
|
|
527
|
+
if (!invocation.writeArtifact)
|
|
528
|
+
throw new Error('Artifact byte destination unsupported');
|
|
529
|
+
await invocation.writeArtifact(bytes, filename);
|
|
530
|
+
},
|
|
531
|
+
...(invocation.workspace ? { mkdir: (path) => invocation.workspace.mkdir(path) } : {}),
|
|
532
|
+
});
|
|
533
|
+
};
|
|
534
|
+
const retireForCommand = async (session, force = false) => {
|
|
535
|
+
const errors = [];
|
|
536
|
+
if (!force)
|
|
537
|
+
try {
|
|
538
|
+
await flushTrace(session);
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
errors.push(error);
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
await (force ? release(session) : checkpointAndRelease(session, local.signal));
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
errors.push(error);
|
|
548
|
+
}
|
|
549
|
+
if (errors.length === 1)
|
|
550
|
+
throw errors[0];
|
|
551
|
+
if (errors.length)
|
|
552
|
+
throw new AggregateError(errors, 'Playwright trace flush and retirement failed');
|
|
553
|
+
};
|
|
554
|
+
const pageResult = async (session, code, snapshot = 'none', filename) => {
|
|
555
|
+
const sections = [];
|
|
556
|
+
if (code)
|
|
557
|
+
sections.push({ title: 'Ran Playwright code', content: code, codeframe: 'js' });
|
|
558
|
+
const page = session.page;
|
|
559
|
+
if (!page)
|
|
560
|
+
return { sections };
|
|
561
|
+
const modal = getPlaywrightModal(page);
|
|
562
|
+
if (modal) {
|
|
563
|
+
sections.push({ title: 'Modal state', content: modal.kind === 'dialog'
|
|
564
|
+
? `- ["${modal.dialog.type()}" dialog with message "${modal.dialog.message()}"]: can be handled by dialog-accept or dialog-dismiss`
|
|
565
|
+
: '- [File chooser]: can be handled by upload' });
|
|
566
|
+
return { sections };
|
|
567
|
+
}
|
|
568
|
+
sections.push(...await downloadSections(session));
|
|
569
|
+
if (invocation.writeArtifact) {
|
|
570
|
+
const consoleLink = await flushPlaywrightConsole(session.lease.context, page, { ...(session.configuration ? { configuration: session.configuration } : {}), writeArtifact: async (bytes, filename) => { await invocation.writeArtifact(bytes, filename); } });
|
|
571
|
+
if (consoleLink)
|
|
572
|
+
sections.push({ title: 'Events', content: `- New console entries: ${consoleLink}` });
|
|
573
|
+
}
|
|
574
|
+
const title = await page.title?.();
|
|
575
|
+
sections.push({ title: 'Page', content: `- Page URL: ${page.url()}${title === undefined ? '' : `\n- Page Title: ${title}`}` });
|
|
576
|
+
if (snapshot !== 'none' && (page.frames || page.ariaSnapshot || page._snapshotForAI || page.ariaSnapshotJSON || session.lease?.captureSnapshotJSON)) {
|
|
577
|
+
const snapshotOptions = { timeout: sessionActionTimeout(session), ...(session.configuration?.snapshot?.boxes === undefined ? {} : { boxes: session.configuration.snapshot.boxes }) };
|
|
578
|
+
if (parsed.command === 'snapshot') {
|
|
579
|
+
if (parsed.options.depth !== undefined) {
|
|
580
|
+
const depth = Number(parsed.options.depth);
|
|
581
|
+
if (!Number.isSafeInteger(depth) || depth < 0)
|
|
582
|
+
throw new Error('Invalid snapshot depth');
|
|
583
|
+
snapshotOptions.depth = depth;
|
|
584
|
+
}
|
|
585
|
+
if (parsed.options.boxes)
|
|
586
|
+
snapshotOptions.boxes = true;
|
|
587
|
+
if (parsed.args[0])
|
|
588
|
+
snapshotOptions.root = await resolveTarget(session, parsed.args[0]);
|
|
589
|
+
}
|
|
590
|
+
if (parsed.command === 'snapshot' && parsed.json && filename === undefined) {
|
|
591
|
+
retained = false;
|
|
592
|
+
const tree = await session.snapshot.captureJSON(page, local.signal, { ...snapshotOptions, ...(session.lease?.captureSnapshotJSON ? { captureJSON: session.lease.captureSnapshotJSON } : {}) });
|
|
593
|
+
check();
|
|
594
|
+
sections.push({ title: 'Snapshot', content: { json: tree }, codeframe: 'json' });
|
|
595
|
+
return { sections };
|
|
596
|
+
}
|
|
597
|
+
const text = await session.snapshot.capture(page, local.signal, snapshotOptions);
|
|
598
|
+
check();
|
|
599
|
+
if (filename !== undefined || snapshot === 'file' && invocation.writeArtifact) {
|
|
600
|
+
const target = filename ?? capabilityArtifactName('page', 'yml', session.configuration);
|
|
601
|
+
const bytes = new TextEncoder().encode(text);
|
|
602
|
+
if (bytes.byteLength > maxArtifactBytes)
|
|
603
|
+
throw new PlaywrightResourceLimitError('Artifact byte limit exceeded');
|
|
604
|
+
if (!invocation.writeArtifact)
|
|
605
|
+
throw new Error('Artifact byte destination unsupported');
|
|
606
|
+
await invocation.writeArtifact(bytes, target);
|
|
607
|
+
sections.push({ title: 'Snapshot', content: `- [Snapshot](${target})` });
|
|
608
|
+
}
|
|
609
|
+
else
|
|
610
|
+
sections.push({ title: 'Snapshot', content: text.trimEnd(), codeframe: 'yaml' });
|
|
611
|
+
}
|
|
612
|
+
return { sections };
|
|
613
|
+
};
|
|
253
614
|
const checkSession = (session) => {
|
|
254
615
|
check();
|
|
255
|
-
if (session.expiresAt !== undefined && session.expiresAt <= Date.now()) {
|
|
616
|
+
if (!session.idlePaused && session.expiresAt !== undefined && session.expiresAt <= Date.now()) {
|
|
256
617
|
session.failure = new Error(`Session expired: ${session.name}; reopen explicitly`);
|
|
257
618
|
void release(session).catch(() => { });
|
|
258
619
|
}
|
|
@@ -260,39 +621,271 @@ export function createPlaywrightController(options = {}) {
|
|
|
260
621
|
if (session.releasing)
|
|
261
622
|
throw session.failure ?? new Error(`Session closed: ${session.name}; reopen explicitly`);
|
|
262
623
|
};
|
|
624
|
+
const resolveTarget = async (session, target) => {
|
|
625
|
+
checkSession(session);
|
|
626
|
+
if (!session.page)
|
|
627
|
+
throw new Error('Selected tab closed; select a tab explicitly');
|
|
628
|
+
const handle = await resolvePlaywrightTarget({ target, page: session.page, timeout: sessionActionTimeout(session), signal: local.signal,
|
|
629
|
+
resolveRef: ref => session.snapshot.resolve(ref), own: handle => ownedTargets.add(handle) });
|
|
630
|
+
checkSession(session);
|
|
631
|
+
if (!targetLocators.has(target)) {
|
|
632
|
+
const reference = isPlaywrightSnapshotRef(target);
|
|
633
|
+
const selector = reference ? session.snapshot.nativeSelector(target) : target;
|
|
634
|
+
let description = `locator(${playwrightCodeString(selector ?? `aria-ref=${target}`)})`;
|
|
635
|
+
if (selector && session.page.locator) {
|
|
636
|
+
const locator = session.page.locator(reference ? selector : playwrightLocatorSelector(session.page, selector));
|
|
637
|
+
if (reference ? locator.normalize || locator._resolveSelector : locator.toString() !== '[object Object]')
|
|
638
|
+
description = await generatedPlaywrightLocator(session.page, selector, reference);
|
|
639
|
+
}
|
|
640
|
+
targetLocators.set(target, `page.${description}`);
|
|
641
|
+
}
|
|
642
|
+
return handle;
|
|
643
|
+
};
|
|
644
|
+
const canRetainAfterError = (session, error) => {
|
|
645
|
+
if (!session || session.state !== 'open' || !session.lease || session.failure || session.releasing || local.signal.aborted
|
|
646
|
+
|| isPlaywrightResourceLimitError(error) || session.pendingActions?.size)
|
|
647
|
+
return false;
|
|
648
|
+
try {
|
|
649
|
+
const pages = session.lease.context.pages();
|
|
650
|
+
return pages.length <= maxTabs && (!session.page || pages.includes(session.page));
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
const runAction = async (session, action) => {
|
|
657
|
+
checkSession(session);
|
|
658
|
+
const page = session.page;
|
|
659
|
+
if (!page)
|
|
660
|
+
throw new Error('Selected tab closed; select a tab explicitly');
|
|
661
|
+
if (getPlaywrightModal(page)) {
|
|
662
|
+
retained = true;
|
|
663
|
+
throw new Error(`Tool "${parsed.command}" does not handle the modal state.`);
|
|
664
|
+
}
|
|
665
|
+
await Promise.all([...session.pendingActions ?? []]);
|
|
666
|
+
checkSession(session);
|
|
667
|
+
let yielded;
|
|
668
|
+
const modal = new Promise(resolve => { yielded = resolve; });
|
|
669
|
+
const unsubscribe = onPlaywrightModal(page, value => { if (value)
|
|
670
|
+
yielded(); });
|
|
671
|
+
const targets = [];
|
|
672
|
+
let settled = false;
|
|
673
|
+
let returnedForModal = false;
|
|
674
|
+
const operation = (async () => {
|
|
675
|
+
const errors = [];
|
|
676
|
+
try {
|
|
677
|
+
await session.snapshot.withReferences(async () => {
|
|
678
|
+
await action();
|
|
679
|
+
// The native CLI allows queued browser events to settle after an action.
|
|
680
|
+
await page.waitForTimeout?.(session.configuration?.timeouts?.settle ?? 500);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
errors.push(error);
|
|
685
|
+
}
|
|
686
|
+
settled = true;
|
|
687
|
+
const results = await Promise.allSettled(targets.map(target => target.dispose()));
|
|
688
|
+
errors.push(...results.flatMap(result => result.status === 'rejected' ? [result.reason] : []));
|
|
689
|
+
if (errors.length === 1)
|
|
690
|
+
throw errors[0];
|
|
691
|
+
if (errors.length)
|
|
692
|
+
throw new AggregateError(errors, 'Playwright action and target cleanup failed');
|
|
693
|
+
})();
|
|
694
|
+
(session.pendingActions ??= new Set()).add(operation);
|
|
695
|
+
void operation.then(() => { session.pendingActions.delete(operation); }, error => {
|
|
696
|
+
session.pendingActions.delete(operation);
|
|
697
|
+
if (returnedForModal && getPlaywrightModal(page) && error instanceof Error && error.name === 'TimeoutError')
|
|
698
|
+
return;
|
|
699
|
+
if (!session.releasing && (returnedForModal || isPlaywrightResourceLimitError(error))) {
|
|
700
|
+
session.failure = error instanceof Error ? error : new Error(String(error));
|
|
701
|
+
void release(session).catch(() => { });
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
try {
|
|
705
|
+
if (getPlaywrightModal(page))
|
|
706
|
+
yielded();
|
|
707
|
+
await Promise.race([operation, modal]);
|
|
708
|
+
checkSession(session);
|
|
709
|
+
returnedForModal = !!getPlaywrightModal(page);
|
|
710
|
+
if (!settled) {
|
|
711
|
+
targets.push(...ownedTargets);
|
|
712
|
+
ownedTargets.clear();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
if (canRetainAfterError(session, error))
|
|
717
|
+
retained = true;
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
finally {
|
|
721
|
+
unsubscribe();
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
const replaceContext = async (session, state) => {
|
|
725
|
+
checkSession(session);
|
|
726
|
+
if (!session.lease?.replaceContext)
|
|
727
|
+
throw new Error('Storage state restoration is unavailable in this browser');
|
|
728
|
+
const restoreRoutes = capturePlaywrightRoutes(session.lease.context);
|
|
729
|
+
const pages = session.lease.context.pages();
|
|
730
|
+
const selected = session.page ? pages.indexOf(session.page) : -1;
|
|
731
|
+
const urls = pages.map(page => page.url());
|
|
732
|
+
session.detachPage?.();
|
|
733
|
+
session.detachContext?.();
|
|
734
|
+
await session.disposeCapabilities?.();
|
|
735
|
+
await session.snapshot.invalidate();
|
|
736
|
+
const context = await session.lease.replaceContext(state, { signal: local.signal });
|
|
737
|
+
checkSession(session);
|
|
738
|
+
await initializeContext(session);
|
|
739
|
+
observeContext(session);
|
|
740
|
+
delete session.page;
|
|
741
|
+
for (const [index, url] of urls.entries()) {
|
|
742
|
+
if (context.pages().length >= maxTabs)
|
|
743
|
+
throw new PlaywrightResourceLimitError('Playwright tab limit exceeded');
|
|
744
|
+
const page = await context.newPage();
|
|
745
|
+
await initializePage(session, page);
|
|
746
|
+
if (index === 0)
|
|
747
|
+
await restoreRoutes?.(context, cleanup => session.cleanups.add(cleanup));
|
|
748
|
+
if (url !== 'about:blank')
|
|
749
|
+
await page.goto(url, { timeout: sessionNavigationTimeout(session) });
|
|
750
|
+
if (index === selected)
|
|
751
|
+
await selectPage(session, page, () => checkSession(session));
|
|
752
|
+
}
|
|
753
|
+
session.pages = [...context.pages()];
|
|
754
|
+
checkSession(session);
|
|
755
|
+
};
|
|
263
756
|
const execute = async () => {
|
|
264
757
|
check();
|
|
265
758
|
if (parsed.command === 'help') {
|
|
266
|
-
|
|
759
|
+
const help = formatPlaywrightHelp(parsed.topic, abilities);
|
|
760
|
+
await invocation.write(parsed.json ? JSON.stringify({ help: help.trimEnd() }, null, 2) + '\n' : help);
|
|
761
|
+
check();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (parsed.command === 'version') {
|
|
765
|
+
await invocation.write(parsed.json ? JSON.stringify({ version: playwrightCliCompatibilityVersion }, null, 2) + '\n' : playwrightCliCompatibilityVersion + '\n');
|
|
267
766
|
check();
|
|
268
767
|
return;
|
|
269
768
|
}
|
|
270
769
|
const ability = abilities.get(parsed.command);
|
|
770
|
+
if (parsed.command === 'attach' && !ability.execute) {
|
|
771
|
+
const targets = [parsed.args[0], parsed.options.cdp, parsed.options.endpoint, parsed.options.extension].filter(Boolean);
|
|
772
|
+
if (targets.length > 1)
|
|
773
|
+
throw new Error('only one of [name], --cdp, --endpoint, or --extension can be specified');
|
|
774
|
+
if (!targets.length || parsed.options.extension === true)
|
|
775
|
+
throw new Error('no target specified for attach command; use one of [name], --cdp, --endpoint, or --extension to specify the target to attach to.');
|
|
776
|
+
throw new Error('An authenticated browser attachment broker is unavailable in this host');
|
|
777
|
+
}
|
|
778
|
+
if (parsed.command === 'detach' && !ability.execute) {
|
|
779
|
+
const known = sessions.has(parsed.session) || (await savedSessions(local.signal)).some(session => session.name === parsed.session);
|
|
780
|
+
if (known)
|
|
781
|
+
throw new Error(parsed.json ? `session '${parsed.session}' was not attached; use close to stop it.` : `session '${parsed.session}' was not attached; use \`playwright-cli${parsed.session === 'default' ? '' : ` -s=${parsed.session}`} close\` to stop it.`);
|
|
782
|
+
await invocation.write(parsed.json ? JSON.stringify({ session: parsed.session, status: 'not-attached' }, null, 2) + '\n' : `Browser '${parsed.session}' is not attached.\n`);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (parsed.command === 'install' && !ability.execute) {
|
|
786
|
+
await initializePlaywrightWorkspace(parsed.options, invocation, Math.min(maxArtifactBytes, maxCommandBytes));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (parsed.command === 'install-browser' && !ability.execute) {
|
|
790
|
+
const browser = parsed.args[0] === 'chrome' ? 'chromium' : parsed.args[0] ?? 'chromium';
|
|
791
|
+
if (!options.adapter || !Object.hasOwn(options.adapter.browsers, browser))
|
|
792
|
+
throw new Error(`Unsupported browser: ${browser}`);
|
|
793
|
+
if (parsed.options.force || parsed.options['with-deps'])
|
|
794
|
+
throw new Error('Browser installation is managed by the configured provider');
|
|
795
|
+
if (parsed.options.list || parsed.options['dry-run'])
|
|
796
|
+
await invocation.write(`Browser: ${browser}\n Install location: provider-managed\n`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
271
799
|
const expiryRetirement = retireExpired();
|
|
272
800
|
if (expiryRetirement)
|
|
273
801
|
await expiryRetirement;
|
|
274
802
|
check();
|
|
275
803
|
if (parsed.command === 'list' && !ability.execute) {
|
|
276
|
-
await
|
|
804
|
+
const saved = await savedSessions(local.signal);
|
|
805
|
+
const known = new Map();
|
|
806
|
+
for (const entry of saved) {
|
|
807
|
+
known.set(entry.name, { name: entry.name, status: 'open' });
|
|
808
|
+
}
|
|
809
|
+
for (const session of sessions.values())
|
|
810
|
+
if (session.state === 'open')
|
|
811
|
+
known.set(session.name, { name: session.name, status: session.state });
|
|
812
|
+
const browsers = [...known.values()];
|
|
813
|
+
await invocation.write(parsed.json ? JSON.stringify({ browsers }, null, 2) + '\n' : browsers.length ? '### Browsers\n' + browsers.map(browser => `- ${browser.name}:\n - status: ${browser.status}\n`).join('') : ' (no browsers)\n');
|
|
277
814
|
check();
|
|
278
815
|
return;
|
|
279
816
|
}
|
|
280
|
-
if (parsed.command === 'close-all' && !ability.execute) {
|
|
817
|
+
if ((parsed.command === 'close-all' || parsed.command === 'kill-all') && !ability.execute) {
|
|
818
|
+
const saved = await savedSessions(local.signal);
|
|
819
|
+
suppressUnknownRestores = true;
|
|
820
|
+
const closed = [...new Set([...saved.map(session => session.name), ...[...sessions.values()].filter(session => session.state === 'open').map(session => session.name)])];
|
|
821
|
+
if (parsed.command === 'kill-all')
|
|
822
|
+
for (const session of sessions.values())
|
|
823
|
+
void release(session).catch(() => { });
|
|
281
824
|
const results = await Promise.allSettled([...new Set([...sessions.keys(), ...tails.keys()])].map(name => enqueue(name, async () => {
|
|
282
825
|
check();
|
|
826
|
+
explicitlyClosed.add(name);
|
|
283
827
|
const session = sessions.get(name);
|
|
284
828
|
if (session)
|
|
285
|
-
await
|
|
829
|
+
await retireForCommand(session, parsed.command === 'kill-all');
|
|
286
830
|
})));
|
|
287
831
|
const errors = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
|
|
832
|
+
try {
|
|
833
|
+
await options.persistence?.close?.(undefined, local.signal);
|
|
834
|
+
}
|
|
835
|
+
catch (error) {
|
|
836
|
+
errors.push(error);
|
|
837
|
+
}
|
|
288
838
|
if (errors.length)
|
|
289
839
|
throw new AggregateError(errors, 'Playwright close-all failed');
|
|
840
|
+
if (parsed.json)
|
|
841
|
+
await invocation.write(JSON.stringify({ closed }, null, 2) + '\n');
|
|
290
842
|
check();
|
|
291
843
|
return;
|
|
292
844
|
}
|
|
293
845
|
await enqueue(parsed.session, async () => {
|
|
294
|
-
|
|
846
|
+
let paused;
|
|
847
|
+
let commandFailure;
|
|
848
|
+
await (async () => {
|
|
295
849
|
check();
|
|
850
|
+
if (options.persistence && ability.scope === 'session' && !['open', 'close', 'close-all', 'list', 'delete-data'].includes(parsed.command)
|
|
851
|
+
&& !explicitlyClosed.has(parsed.session) && (!suppressUnknownRestores || sessions.has(parsed.session))
|
|
852
|
+
&& (!sessions.has(parsed.session) || sessions.get(parsed.session).state === 'closed')) {
|
|
853
|
+
const restored = await options.persistence.restore({ name: parsed.session, signal: local.signal });
|
|
854
|
+
if (restored) {
|
|
855
|
+
let transferred = false;
|
|
856
|
+
try {
|
|
857
|
+
await restoreSession({ name: parsed.session, signal: local.signal,
|
|
858
|
+
...(restored.expiresAt === undefined ? {} : { expiresAt: restored.expiresAt }),
|
|
859
|
+
...(restored.idleTimeoutMs === undefined ? {} : { idleTimeoutMs: restored.idleTimeoutMs }),
|
|
860
|
+
...(restored.contextOptions === undefined ? {} : { contextOptions: restored.contextOptions }),
|
|
861
|
+
...(restored.configuration === undefined ? {} : { configuration: restored.configuration }),
|
|
862
|
+
acquire: async () => { transferred = true; return restored; } }, true);
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
if (!transferred) {
|
|
866
|
+
try {
|
|
867
|
+
await restored.lease.release();
|
|
868
|
+
}
|
|
869
|
+
catch (cleanupError) {
|
|
870
|
+
throw new AggregateError([error, cleanupError], 'Playwright restoration and cleanup failed');
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
check();
|
|
877
|
+
}
|
|
878
|
+
const current = sessions.get(parsed.session);
|
|
879
|
+
if (current?.state === 'open' && current.idleTimeoutMs) {
|
|
880
|
+
paused = current;
|
|
881
|
+
current.idlePaused = true;
|
|
882
|
+
clearTimeout(current.expiryTimer);
|
|
883
|
+
}
|
|
884
|
+
const modalPage = current?.page;
|
|
885
|
+
if (modalPage && getPlaywrightModal(modalPage) && !['dialog-accept', 'dialog-dismiss', 'upload', 'snapshot', 'close', 'delete-data', 'tab-select', 'tab-close'].includes(parsed.command)) {
|
|
886
|
+
retained = true;
|
|
887
|
+
throw new Error(`Tool "${parsed.command}" does not handle the modal state.`);
|
|
888
|
+
}
|
|
296
889
|
if (ability.execute) {
|
|
297
890
|
let browserSession;
|
|
298
891
|
if (ability.scope === 'session') {
|
|
@@ -313,14 +906,43 @@ export function createPlaywrightController(options = {}) {
|
|
|
313
906
|
browserSession = Object.freeze({
|
|
314
907
|
context: session.lease.context,
|
|
315
908
|
page: session.page,
|
|
316
|
-
|
|
909
|
+
...(session.configuration ? { configuration: session.configuration } : {}),
|
|
910
|
+
resolveTarget(ref) { return resolveTarget(session, ref); },
|
|
911
|
+
targetLocator(target) {
|
|
912
|
+
const locator = targetLocators.get(target);
|
|
913
|
+
if (!locator)
|
|
914
|
+
throw new Error('Target must be resolved before describing its locator');
|
|
915
|
+
return locator;
|
|
916
|
+
},
|
|
917
|
+
replaceContext(state) { return replaceContext(session, state); },
|
|
918
|
+
async invalidateTargets() { checkSession(session); await session.snapshot.invalidate(); checkSession(session); },
|
|
919
|
+
runAction(action) { return runAction(session, action); },
|
|
920
|
+
...(session.lease.executeCode ? { executeCode: ((executionOptions) => {
|
|
921
|
+
checkSession(session);
|
|
922
|
+
return session.lease.executeCode(executionOptions);
|
|
923
|
+
}) } : {}),
|
|
924
|
+
...(session.lease.generateActionCode ? { generateActionCode: session.lease.generateActionCode } : {}),
|
|
925
|
+
...(session.lease.captureTrace ? { captureTrace: session.lease.captureTrace } : {}),
|
|
926
|
+
...(session.lease.prepareFileBytes ? { prepareFileBytes(bytes) {
|
|
927
|
+
checkSession(session);
|
|
928
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength > maxArtifactBytes || bytes.byteLength > maxCommandBytes)
|
|
929
|
+
throw new PlaywrightResourceLimitError('Artifact byte limit exceeded');
|
|
930
|
+
return session.lease.prepareFileBytes(bytes);
|
|
931
|
+
} } : {}),
|
|
932
|
+
...(session.lease.captureArtifact ? { captureArtifact: ((produce, captureOptions) => {
|
|
933
|
+
checkSession(session);
|
|
934
|
+
return session.lease.captureArtifact(produce, { ...captureOptions,
|
|
935
|
+
signal: AbortSignal.any([local.signal, captureOptions.signal]),
|
|
936
|
+
maxBytes: Math.min(captureOptions.maxBytes, maxArtifactBytes, maxCommandBytes),
|
|
937
|
+
});
|
|
938
|
+
}) } : {}),
|
|
317
939
|
async selectPage(page) {
|
|
318
940
|
checkSession(session);
|
|
319
941
|
const pages = session.lease.context.pages();
|
|
320
942
|
if (!pages.includes(page))
|
|
321
943
|
throw new Error('Page does not belong to this session');
|
|
322
944
|
if (pages.length > maxTabs)
|
|
323
|
-
throw new
|
|
945
|
+
throw new PlaywrightResourceLimitError('Playwright tab limit exceeded');
|
|
324
946
|
await selectPage(session, page, () => checkSession(session));
|
|
325
947
|
session.pages = [...pages];
|
|
326
948
|
},
|
|
@@ -332,33 +954,110 @@ export function createPlaywrightController(options = {}) {
|
|
|
332
954
|
},
|
|
333
955
|
});
|
|
334
956
|
}
|
|
335
|
-
const executeAbility = () => executePlaywrightAbility(ability, parsed, invocation, { signal: local.signal, maxCommandBytes, maxArtifactBytes, ...(browserSession ? { browserSession } : {}), check: () => active ? checkSession(active) : check() });
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
957
|
+
const executeAbility = () => executePlaywrightAbility(ability, parsed, invocation, { signal: local.signal, maxCommandBytes, maxArtifactBytes, actionTimeoutMs: sessionActionTimeout(active), codeExecutionTimeoutMs, navigationTimeoutMs: sessionNavigationTimeout(active), maxPages: maxTabs, ...(browserSession ? { browserSession } : {}), check: () => active ? checkSession(active) : check() });
|
|
958
|
+
let result;
|
|
959
|
+
try {
|
|
960
|
+
result = active ? await active.snapshot.withReferences(executeAbility) : await executeAbility();
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
if (canRetainAfterError(active, error))
|
|
964
|
+
retained = true;
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
340
967
|
check();
|
|
341
968
|
if (active) {
|
|
969
|
+
if (!active.page || !getPlaywrightModal(active.page))
|
|
970
|
+
await Promise.all([...active.pendingActions ?? []]);
|
|
342
971
|
await active.snapshot.invalidate();
|
|
343
972
|
checkSession(active);
|
|
344
973
|
active.pages = [...active.lease.context.pages()];
|
|
345
974
|
if (active.pages.length > maxTabs)
|
|
346
|
-
throw new
|
|
975
|
+
throw new PlaywrightResourceLimitError('Playwright tab limit exceeded');
|
|
976
|
+
await checkpoint(active, local.signal);
|
|
977
|
+
if (active.page && getPlaywrightModal(active.page))
|
|
978
|
+
result = { sections: [...result?.sections ?? [], ...(await pageResult(active, undefined)).sections] };
|
|
979
|
+
else {
|
|
980
|
+
const downloads = await downloadSections(active);
|
|
981
|
+
if (downloads.length)
|
|
982
|
+
result = { sections: [...result?.sections ?? [], ...downloads] };
|
|
983
|
+
}
|
|
984
|
+
const traceLinks = await flushTrace(active);
|
|
985
|
+
traceFlushed = true;
|
|
986
|
+
if (traceLinks.length && ['tracing-start', 'tracing-stop'].includes(parsed.command)) {
|
|
987
|
+
const sections = [...result?.sections ?? []];
|
|
988
|
+
const index = sections.findIndex(section => section.title === 'Result');
|
|
989
|
+
if (index === -1)
|
|
990
|
+
sections.push({ title: 'Result', content: traceLinks.join('\n') });
|
|
991
|
+
else
|
|
992
|
+
sections[index] = { ...sections[index], content: sections[index].content + '\n' + traceLinks.join('\n') };
|
|
993
|
+
result = { sections };
|
|
994
|
+
}
|
|
347
995
|
}
|
|
996
|
+
if (result)
|
|
997
|
+
await writeResult(result);
|
|
348
998
|
retained = true;
|
|
349
999
|
return;
|
|
350
1000
|
}
|
|
351
1001
|
if (parsed.command === 'close') {
|
|
352
1002
|
const session = sessions.get(parsed.session);
|
|
353
|
-
|
|
354
|
-
|
|
1003
|
+
const wasOpen = session?.state === 'open' || (await savedSessions(local.signal)).some(saved => saved.name === parsed.session);
|
|
1004
|
+
explicitlyClosed.add(parsed.session);
|
|
1005
|
+
const errors = [];
|
|
1006
|
+
try {
|
|
1007
|
+
if (session)
|
|
1008
|
+
await retireForCommand(session);
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
errors.push(error);
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
await options.persistence?.close?.(parsed.session, local.signal);
|
|
1015
|
+
}
|
|
1016
|
+
catch (error) {
|
|
1017
|
+
errors.push(error);
|
|
1018
|
+
}
|
|
1019
|
+
if (errors.length === 1)
|
|
1020
|
+
throw errors[0];
|
|
1021
|
+
if (errors.length)
|
|
1022
|
+
throw new AggregateError(errors, 'Playwright close failed');
|
|
1023
|
+
await invocation.write(parsed.json ? JSON.stringify({ session: parsed.session, status: wasOpen ? 'closed' : 'not-open' }, null, 2) + '\n'
|
|
1024
|
+
: wasOpen ? `Browser '${parsed.session}' closed\n\n` : `Browser '${parsed.session}' is not open.\n`);
|
|
1025
|
+
check();
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (parsed.command === 'delete-data') {
|
|
1029
|
+
const session = sessions.get(parsed.session);
|
|
1030
|
+
explicitlyClosed.add(parsed.session);
|
|
1031
|
+
const errors = [];
|
|
1032
|
+
try {
|
|
1033
|
+
if (session && session.state !== 'closed')
|
|
1034
|
+
await release(session);
|
|
1035
|
+
}
|
|
1036
|
+
catch (error) {
|
|
1037
|
+
errors.push(error);
|
|
1038
|
+
}
|
|
1039
|
+
try {
|
|
1040
|
+
await options.persistence?.delete(parsed.session, local.signal);
|
|
1041
|
+
sessions.delete(parsed.session);
|
|
1042
|
+
}
|
|
1043
|
+
catch (error) {
|
|
1044
|
+
errors.push(error);
|
|
1045
|
+
}
|
|
1046
|
+
if (errors.length === 1)
|
|
1047
|
+
throw errors[0];
|
|
1048
|
+
if (errors.length)
|
|
1049
|
+
throw new AggregateError(errors, 'Playwright retirement and data deletion failed');
|
|
1050
|
+
await invocation.write(parsed.json ? JSON.stringify({ session: parsed.session, deleted: !!session || !!options.persistence }, null, 2) + '\n'
|
|
1051
|
+
: `Deleted user data for browser '${parsed.session}'.\n`);
|
|
355
1052
|
check();
|
|
356
1053
|
return;
|
|
357
1054
|
}
|
|
358
1055
|
if (parsed.command === 'open') {
|
|
1056
|
+
const openOptions = await resolvePlaywrightOpenOptions(parsed.options, invocation, options.adapter, Math.min(maxArtifactBytes, maxCommandBytes));
|
|
1057
|
+
explicitlyClosed.delete(parsed.session);
|
|
359
1058
|
const previous = sessions.get(parsed.session);
|
|
360
1059
|
if (previous && previous.state !== 'closed')
|
|
361
|
-
|
|
1060
|
+
await retireForCommand(previous);
|
|
362
1061
|
// Remote-loss retirement must finish before reusing its capacity.
|
|
363
1062
|
if (previous?.releasing)
|
|
364
1063
|
await previous.releasing;
|
|
@@ -367,25 +1066,31 @@ export function createPlaywrightController(options = {}) {
|
|
|
367
1066
|
// claim this slot during retirement's asynchronous boundary.
|
|
368
1067
|
const occupied = [...sessions.values()].filter(s => s.state !== 'closed').length;
|
|
369
1068
|
if (occupied >= maxSessions)
|
|
370
|
-
throw new
|
|
371
|
-
active = { name: parsed.session, generation: ++generation, state: 'acquiring', snapshot: createSnapshotEngine({ maxSnapshotBytes, maxSnapshotRefs }, () => `e${++refSequence}`), cleanups: new Set() };
|
|
1069
|
+
throw new PlaywrightResourceLimitError('Playwright session capacity exceeded');
|
|
1070
|
+
active = { name: parsed.session, generation: ++generation, state: 'acquiring', snapshot: createSnapshotEngine({ maxSnapshotBytes, maxSnapshotRefs }, () => `e${++refSequence}`), cleanups: new Set(), idleTimeoutMs: openOptions.idleTimeoutMs, contextOptions: openOptions.contextOptions };
|
|
1071
|
+
active.configuration = { ...openOptions.configuration, browserName: openOptions.browser, headless: openOptions.headless };
|
|
372
1072
|
sessions.set(parsed.session, active);
|
|
373
1073
|
const session = active;
|
|
374
|
-
session.lease = await options.adapter.acquire({ acquisitionId: `playwright-${session.generation}`, session: session.name, browser:
|
|
1074
|
+
session.lease = await options.adapter.acquire({ acquisitionId: `playwright-${session.generation}`, session: session.name, browser: openOptions.browser, headless: openOptions.headless, contextOptions: openOptions.contextOptions, signal: local.signal });
|
|
375
1075
|
check();
|
|
376
1076
|
observeSession(session);
|
|
1077
|
+
await initializeContext(session);
|
|
377
1078
|
checkSession(session);
|
|
378
1079
|
if (session.lease.context.pages().length >= maxTabs)
|
|
379
|
-
throw new
|
|
1080
|
+
throw new PlaywrightResourceLimitError('Playwright tab limit exceeded');
|
|
380
1081
|
await selectPage(session, await session.lease.context.newPage(), () => checkSession(session));
|
|
381
1082
|
session.pages = [...session.lease.context.pages()];
|
|
382
1083
|
checkSession(session);
|
|
383
1084
|
if (parsed.url)
|
|
384
|
-
await session.page.goto(parsed.url, { timeout:
|
|
1085
|
+
await runAction(session, async () => { await session.page.goto(parsed.url, { timeout: sessionNavigationTimeout(session) }); });
|
|
385
1086
|
checkSession(session);
|
|
386
|
-
await
|
|
1087
|
+
const result = await pageResult(session, actionCode(session, { name: 'navigate', url: parsed.url ?? 'about:blank' }, `await page.goto(${playwrightCodeString(parsed.url ?? 'about:blank')});`), 'file');
|
|
1088
|
+
const rendered = serializePlaywrightResult(configuredResult(result), parsed);
|
|
1089
|
+
await invocation.write(parsed.json ? JSON.stringify({ session: session.name, result: JSON.parse(rendered) }, null, 2) + '\n'
|
|
1090
|
+
: `### Browser \`${session.name}\` opened.\n` + rendered);
|
|
387
1091
|
checkSession(session);
|
|
388
1092
|
session.state = 'open';
|
|
1093
|
+
await checkpoint(session, local.signal);
|
|
389
1094
|
retained = true;
|
|
390
1095
|
}
|
|
391
1096
|
else {
|
|
@@ -400,10 +1105,11 @@ export function createPlaywrightController(options = {}) {
|
|
|
400
1105
|
}
|
|
401
1106
|
await session.snapshot.invalidate();
|
|
402
1107
|
checkSession(session);
|
|
403
|
-
await session.page.goto(parsed.url, { timeout:
|
|
1108
|
+
await runAction(session, async () => { await session.page.goto(parsed.url, { timeout: sessionNavigationTimeout(session) }); });
|
|
404
1109
|
checkSession(session);
|
|
405
|
-
await
|
|
1110
|
+
await writeResult(await pageResult(session, actionCode(session, { name: 'navigate', url: parsed.url }, `await page.goto(${playwrightCodeString(parsed.url)});`), 'file'));
|
|
406
1111
|
checkSession(session);
|
|
1112
|
+
await checkpoint(session, local.signal);
|
|
407
1113
|
retained = true;
|
|
408
1114
|
return;
|
|
409
1115
|
}
|
|
@@ -419,12 +1125,31 @@ export function createPlaywrightController(options = {}) {
|
|
|
419
1125
|
throw new Error('Selected tab closed; select a tab explicitly');
|
|
420
1126
|
}
|
|
421
1127
|
checkSession(session);
|
|
422
|
-
if (parsed.command === '
|
|
423
|
-
|
|
1128
|
+
if (parsed.command === 'config-print') {
|
|
1129
|
+
const config = { browser: {
|
|
1130
|
+
...(session.configuration?.browserName ? { browserName: session.configuration.browserName } : {}),
|
|
1131
|
+
isolated: true,
|
|
1132
|
+
launchOptions: session.configuration?.headless === undefined ? {} : { headless: session.configuration.headless },
|
|
1133
|
+
contextOptions: session.contextOptions ?? {},
|
|
1134
|
+
...(session.configuration?.initScriptFiles ? { initScript: session.configuration.initScriptFiles } : {}),
|
|
1135
|
+
...(session.configuration?.initPages ? { initPage: session.configuration.initPages.map(page => page.filename) } : {}),
|
|
1136
|
+
}, codegen: session.configuration?.codegen ?? 'typescript',
|
|
1137
|
+
timeouts: { action: sessionActionTimeout(session), navigation: sessionNavigationTimeout(session), expect: session.configuration?.timeouts?.expect ?? 5000, settle: session.configuration?.timeouts?.settle ?? 500, idle: session.idleTimeoutMs ?? 0 },
|
|
1138
|
+
snapshot: { mode: 'full', ...session.configuration?.snapshot }, skillMode: true,
|
|
1139
|
+
...Object.fromEntries(['network', 'console', 'outputDir', 'outputMaxSize', 'testIdAttribute', 'configFile'].flatMap(key => {
|
|
1140
|
+
const value = session.configuration?.[key];
|
|
1141
|
+
return value === undefined ? [] : [[key, value]];
|
|
1142
|
+
})),
|
|
1143
|
+
};
|
|
1144
|
+
await writeResult({ sections: [{ title: 'Result', content: { json: config } }] });
|
|
1145
|
+
}
|
|
1146
|
+
else if (parsed.command === 'tab-list') {
|
|
1147
|
+
const tabs = await Promise.all(pages.map(async (tab, index) => `- ${index}:${tab === page ? ' (current)' : ''} [${await tab.title?.() ?? ''}](${tab.url()})`));
|
|
1148
|
+
await writeResult({ sections: [{ title: 'Result', content: tabs.join('\n') }] });
|
|
424
1149
|
}
|
|
425
1150
|
else if (parsed.command === 'tab-new') {
|
|
426
1151
|
if (pages.length >= maxTabs)
|
|
427
|
-
throw new
|
|
1152
|
+
throw new PlaywrightResourceLimitError('Playwright tab limit exceeded');
|
|
428
1153
|
retained = false;
|
|
429
1154
|
await session.snapshot.invalidate();
|
|
430
1155
|
checkSession(session);
|
|
@@ -432,9 +1157,9 @@ export function createPlaywrightController(options = {}) {
|
|
|
432
1157
|
await selectPage(session, tab, () => checkSession(session));
|
|
433
1158
|
session.pages = [...session.lease.context.pages()];
|
|
434
1159
|
if (parsed.url)
|
|
435
|
-
await tab.goto(parsed.url, { timeout:
|
|
1160
|
+
await runAction(session, async () => { await tab.goto(parsed.url, { timeout: sessionNavigationTimeout(session) }); });
|
|
436
1161
|
checkSession(session);
|
|
437
|
-
await
|
|
1162
|
+
await writeResult(await pageResult(session, 'await page.context().newPage();', 'file'));
|
|
438
1163
|
}
|
|
439
1164
|
else if (parsed.command === 'tab-select' || parsed.command === 'tab-close') {
|
|
440
1165
|
const index = parsed.tab ?? pages.indexOf(page);
|
|
@@ -459,58 +1184,160 @@ export function createPlaywrightController(options = {}) {
|
|
|
459
1184
|
else
|
|
460
1185
|
await selectPage(session, tab, () => checkSession(session));
|
|
461
1186
|
checkSession(session);
|
|
462
|
-
await
|
|
1187
|
+
await writeResult(await pageResult(session, parsed.command === 'tab-close' ? `await page.context().pages()[${index}].close();` : undefined, 'file'));
|
|
1188
|
+
}
|
|
1189
|
+
else if (parsed.command === 'highlight') {
|
|
1190
|
+
const target = parsed.args[0];
|
|
1191
|
+
const hide = parsed.options.hide === true;
|
|
1192
|
+
if (!target && !hide)
|
|
1193
|
+
throw new Error('Highlight requires a target unless --hide is used');
|
|
1194
|
+
const style = parsed.options.style;
|
|
1195
|
+
if (!target) {
|
|
1196
|
+
if (page.hideHighlight)
|
|
1197
|
+
await page.hideHighlight();
|
|
1198
|
+
else
|
|
1199
|
+
await updatePlaywrightHighlight(page, undefined, { hide });
|
|
1200
|
+
await writeResult({ sections: [{ title: 'Result', content: 'Hid page highlight' }] });
|
|
1201
|
+
}
|
|
1202
|
+
else {
|
|
1203
|
+
const handle = await resolveTarget(session, target);
|
|
1204
|
+
const isReference = isPlaywrightSnapshotRef(target);
|
|
1205
|
+
const selector = isReference ? session.snapshot.nativeSelector(target) : target;
|
|
1206
|
+
if (!selector)
|
|
1207
|
+
throw new Error('Native locator generation unavailable for this snapshot');
|
|
1208
|
+
const description = await generatedPlaywrightLocator(page, selector, isReference);
|
|
1209
|
+
const locator = page.locator(isReference ? selector : playwrightLocatorSelector(page, selector));
|
|
1210
|
+
if (page.hideHighlight && locator.hideHighlight && locator.highlight) {
|
|
1211
|
+
if (hide)
|
|
1212
|
+
await locator.hideHighlight();
|
|
1213
|
+
else
|
|
1214
|
+
await locator.highlight(style === undefined ? {} : { style });
|
|
1215
|
+
}
|
|
1216
|
+
else
|
|
1217
|
+
await updatePlaywrightHighlight(page, handle, { hide, ...(style === undefined ? {} : { style }) });
|
|
1218
|
+
await writeResult({ sections: [{ title: 'Result', content: hide ? `Hid highlight for ${description}` : `Highlighted ${description}` }] });
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
else if (parsed.command === 'generate-locator') {
|
|
1222
|
+
const target = parsed.args[0];
|
|
1223
|
+
await resolveTarget(session, target);
|
|
1224
|
+
const isReference = isPlaywrightSnapshotRef(target);
|
|
1225
|
+
const selector = isReference ? session.snapshot.nativeSelector(target) : target;
|
|
1226
|
+
if (!selector)
|
|
1227
|
+
throw new Error('Native locator generation unavailable for this snapshot');
|
|
1228
|
+
const result = await generatedPlaywrightLocator(page, selector, isReference);
|
|
1229
|
+
checkSession(session);
|
|
1230
|
+
await writeResult({ sections: [{ title: 'Result', content: result }] });
|
|
1231
|
+
}
|
|
1232
|
+
else if (parsed.command === 'find') {
|
|
1233
|
+
const text = await session.snapshot.capture(page, local.signal);
|
|
1234
|
+
const result = await findPlaywrightSnapshot(text, { page: page, ...(parsed.args[0] === undefined ? {} : { text: parsed.args[0] }),
|
|
1235
|
+
...(parsed.options.regex === undefined ? {} : { regex: parsed.options.regex }),
|
|
1236
|
+
});
|
|
1237
|
+
checkSession(session);
|
|
1238
|
+
await writeResult({ sections: [{ title: 'Result', content: result }] });
|
|
463
1239
|
}
|
|
464
1240
|
else if (parsed.command === 'snapshot') {
|
|
465
1241
|
if (typeof page.on !== 'function' || typeof page.off !== 'function')
|
|
466
1242
|
throw new Error('Snapshot engine unsupported: navigation events required');
|
|
467
|
-
|
|
468
|
-
checkSession(session);
|
|
469
|
-
if (parsed.filename !== undefined) {
|
|
470
|
-
const bytes = new TextEncoder().encode(text);
|
|
471
|
-
if (bytes.byteLength > maxArtifactBytes)
|
|
472
|
-
throw new Error('Artifact byte limit exceeded');
|
|
473
|
-
await invocation.writeArtifact(bytes, parsed.filename);
|
|
474
|
-
}
|
|
475
|
-
else
|
|
476
|
-
await invocation.write(text);
|
|
1243
|
+
await session.snapshot.withReferences(async () => { await writeResult(await pageResult(session, undefined, 'inline', parsed.filename)); });
|
|
477
1244
|
}
|
|
478
1245
|
else if (parsed.command === 'click' || parsed.command === 'fill') {
|
|
479
1246
|
const command = parsed.command;
|
|
480
1247
|
await session.snapshot.withReferences(async () => {
|
|
481
|
-
const target = await session
|
|
1248
|
+
const target = await resolveTarget(session, parsed.ref);
|
|
482
1249
|
if (typeof target[command] !== 'function')
|
|
483
1250
|
throw new Error('Snapshot action unsupported');
|
|
484
1251
|
checkSession(session);
|
|
485
1252
|
retained = false;
|
|
486
1253
|
if (command === 'click')
|
|
487
|
-
await target.click({ timeout:
|
|
488
|
-
else
|
|
489
|
-
await
|
|
1254
|
+
await runAction(session, () => target.click({ timeout: sessionActionTimeout(session), ...(parsed.button === 'left' ? {} : { button: parsed.button }), ...(parsed.modifiers?.length ? { modifiers: parsed.modifiers } : {}) }));
|
|
1255
|
+
else {
|
|
1256
|
+
await runAction(session, async () => {
|
|
1257
|
+
await target.fill(parsed.value, { timeout: sessionActionTimeout(session) });
|
|
1258
|
+
if (parsed.options.submit)
|
|
1259
|
+
await page.keyboard.press('Enter');
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
490
1262
|
});
|
|
491
1263
|
}
|
|
492
1264
|
else if (parsed.command === 'press') {
|
|
493
1265
|
if (typeof page.keyboard?.press !== 'function')
|
|
494
1266
|
throw new Error('Keyboard press unsupported');
|
|
495
1267
|
retained = false;
|
|
496
|
-
await page.keyboard.press(parsed.value);
|
|
1268
|
+
await runAction(session, () => page.keyboard.press(parsed.value));
|
|
497
1269
|
}
|
|
498
1270
|
else if (parsed.command === 'screenshot') {
|
|
499
|
-
const bytes =
|
|
1271
|
+
const bytes = parsed.ref
|
|
1272
|
+
? await capturePlaywrightTargetScreenshot(await resolveTarget(session, parsed.ref), { type: parsed.imageType, scale: parsed.scale, timeout: sessionActionTimeout(session), maxArtifactBytes, signal: local.signal })
|
|
1273
|
+
: await capturePlaywrightScreenshot(page, { type: parsed.imageType, fullPage: parsed.fullPage, scale: parsed.scale, timeout: sessionActionTimeout(session), maxArtifactBytes, signal: local.signal });
|
|
500
1274
|
checkSession(session);
|
|
501
1275
|
// Uint8Array constructor copies even when bytes is a Buffer view.
|
|
502
|
-
|
|
1276
|
+
const filename = parsed.filename ?? capabilityArtifactName(parsed.ref ? 'element' : 'page', parsed.imageType, session.configuration);
|
|
1277
|
+
await invocation.writeArtifact(new Uint8Array(bytes), filename);
|
|
1278
|
+
const label = parsed.ref ? 'element' : parsed.fullPage ? 'full page' : 'viewport';
|
|
1279
|
+
const screenshotTarget = parsed.ref ? targetLocators.get(parsed.ref) : 'page';
|
|
1280
|
+
await writeResult({ sections: [
|
|
1281
|
+
{ title: 'Result', content: `- [Screenshot of ${label}](${filename})` },
|
|
1282
|
+
{ title: 'Ran Playwright code', content: `await ${screenshotTarget}.screenshot({ path: ${playwrightCodeString(filename)}, type: ${playwrightCodeString(parsed.imageType)},${parsed.imageType === 'jpeg' ? ' quality: 90,' : ''} scale: ${playwrightCodeString(parsed.scale)}${parsed.ref ? '' : `, fullPage: ${parsed.fullPage}`} });`, codeframe: 'js' },
|
|
1283
|
+
] });
|
|
503
1284
|
}
|
|
504
1285
|
checkSession(session);
|
|
1286
|
+
if (parsed.command === 'click' || parsed.command === 'fill' || parsed.command === 'press') {
|
|
1287
|
+
const clickOptions = { ...(parsed.button && parsed.button !== 'left' ? { button: parsed.button } : {}), ...(parsed.modifiers?.length ? { modifiers: parsed.modifiers } : {}) };
|
|
1288
|
+
const args = parsed.command === 'fill' ? playwrightCodeString(parsed.value) : Object.keys(clickOptions).length ? JSON.stringify(clickOptions) : '';
|
|
1289
|
+
let code = parsed.command === 'press' ? `await page.keyboard.press(${playwrightCodeString(parsed.value)});`
|
|
1290
|
+
: `await ${targetLocators.get(parsed.ref)}.${parsed.command}(${args});${parsed.command === 'fill' && parsed.options.submit ? `\nawait page.keyboard.press('Enter');` : ''}`;
|
|
1291
|
+
if (parsed.command !== 'press' && session.lease?.generateActionCode) {
|
|
1292
|
+
const selector = playwrightLocatorSelector(page, targetLocators.get(parsed.ref).slice('page.'.length));
|
|
1293
|
+
if (parsed.command === 'click') {
|
|
1294
|
+
const modifiers = parsed.modifiers ?? [];
|
|
1295
|
+
const mask = (modifiers.includes('Alt') ? 1 : 0) | (modifiers.includes('Control') || modifiers.includes('ControlOrMeta') ? 2 : 0) | (modifiers.includes('Meta') ? 4 : 0) | (modifiers.includes('Shift') ? 8 : 0);
|
|
1296
|
+
code = actionCode(session, { name: 'click', selector, button: parsed.button ?? 'left', modifiers: mask, clickCount: 1 }, code);
|
|
1297
|
+
}
|
|
1298
|
+
else {
|
|
1299
|
+
const base = `await ${targetLocators.get(parsed.ref)}.fill(${args});`;
|
|
1300
|
+
code = actionCode(session, { name: 'fill', selector, text: parsed.value }, base);
|
|
1301
|
+
if (parsed.options.submit)
|
|
1302
|
+
code += '\n' + actionCode(session, { name: 'press', selector, key: 'Enter', modifiers: 0 }, "await page.keyboard.press('Enter');");
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
await writeResult(await pageResult(session, code, 'file'));
|
|
1306
|
+
}
|
|
1307
|
+
await checkpoint(session, local.signal);
|
|
505
1308
|
retained = true;
|
|
506
1309
|
}
|
|
1310
|
+
})().catch(error => {
|
|
1311
|
+
commandFailure = { error };
|
|
1312
|
+
});
|
|
1313
|
+
let traceFailure;
|
|
1314
|
+
try {
|
|
1315
|
+
if (active?.state === 'open' && retained && !traceFlushed)
|
|
1316
|
+
await flushTrace(active);
|
|
507
1317
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
1318
|
+
catch (error) {
|
|
1319
|
+
retained = false;
|
|
1320
|
+
traceFailure = { error };
|
|
1321
|
+
}
|
|
1322
|
+
if (paused) {
|
|
1323
|
+
delete paused.idlePaused;
|
|
1324
|
+
if (paused.state === 'open') {
|
|
1325
|
+
paused.expiresAt = Date.now() + paused.idleTimeoutMs;
|
|
1326
|
+
scheduleExpiry(paused);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
// Retirement is part of the session queue. Keep its rejection for
|
|
1330
|
+
// shared invocation cleanup so both execution and cleanup causes survive.
|
|
1331
|
+
if (active && !retained)
|
|
1332
|
+
await release(active).catch(() => { });
|
|
1333
|
+
if (traceFailure) {
|
|
1334
|
+
if (commandFailure)
|
|
1335
|
+
throw new AggregateError([commandFailure.error, traceFailure.error], 'Playwright command and trace flush failed');
|
|
1336
|
+
throw traceFailure.error;
|
|
513
1337
|
}
|
|
1338
|
+
if (commandFailure)
|
|
1339
|
+
throw commandFailure.error;
|
|
1340
|
+
await enforceOutputBudget();
|
|
514
1341
|
});
|
|
515
1342
|
};
|
|
516
1343
|
const operation = execute();
|
|
@@ -546,9 +1373,17 @@ export function createPlaywrightController(options = {}) {
|
|
|
546
1373
|
return disposal;
|
|
547
1374
|
// Install shared completion before notifying potentially reentrant hosts.
|
|
548
1375
|
disposal = Promise.resolve().then(async () => {
|
|
549
|
-
const retirements = [...sessions.values()].filter(s => s.lease).map(
|
|
1376
|
+
const retirements = [...sessions.values()].filter(s => s.lease).map(async (session) => {
|
|
1377
|
+
try {
|
|
1378
|
+
await checkpoint(session, new AbortController().signal, false);
|
|
1379
|
+
}
|
|
1380
|
+
finally {
|
|
1381
|
+
await release(session);
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
550
1384
|
await Promise.allSettled([...work]);
|
|
551
|
-
const results = await Promise.allSettled(
|
|
1385
|
+
const results = await Promise.allSettled(retirements);
|
|
1386
|
+
results.push(...await Promise.allSettled([...sessions.values()].map(release)));
|
|
552
1387
|
const errors = [...new Set(results.flatMap(result => result.status === 'rejected' ? [result.reason] : []))];
|
|
553
1388
|
if (errors.length)
|
|
554
1389
|
throw new AggregateError(errors, 'Playwright disposal failed');
|
|
@@ -556,6 +1391,7 @@ export function createPlaywrightController(options = {}) {
|
|
|
556
1391
|
lifetime.abort(new Error('Playwright controller is disposed'));
|
|
557
1392
|
return disposal;
|
|
558
1393
|
};
|
|
559
|
-
return { run, dispose, restoreSession, inspectSessions };
|
|
1394
|
+
return { run, dispose, restoreSession: (request) => restoreSession(request), inspectSessions };
|
|
560
1395
|
}
|
|
1396
|
+
import { PlaywrightResourceLimitError, isPlaywrightResourceLimitError } from './resource-limit.js';
|
|
561
1397
|
//# sourceMappingURL=controller.js.map
|