@ultimat3/scraping 23.0.0 → 25.0.0

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/src/target.ts CHANGED
@@ -15,6 +15,18 @@ import type { SessionSnapshot } from './session-state';
15
15
  */
16
16
  export const ROOT_SELECTOR = 'html';
17
17
 
18
+ /** What a `query()` may do to the page before it measures. */
19
+ export interface QueryOptions {
20
+ /**
21
+ * Scroll the FIRST match into the viewport before measuring. `elementFromPoint` answers nothing
22
+ * for a point off-screen, so without it every element below the fold read `hitTarget: false` —
23
+ * "covered" — and each act on it waited out its whole timeout as `X_SCRAPE_NOT_ACTIONABLE`.
24
+ * Only the `actionable` wait asks (`page-over-target.ts`): a READ must not move the page, or the
25
+ * next screenshot is of somewhere else. A driver with no layout engine ignores it.
26
+ */
27
+ readonly reveal?: boolean | undefined;
28
+ }
29
+
18
30
  export interface ElementBox {
19
31
  readonly x: number;
20
32
  readonly y: number;
@@ -105,7 +117,7 @@ export interface GotoOptions {
105
117
  * so `page.screenshot({ timeout })` was a documented deadline that bounded nothing. Deleted
106
118
  * rather than implemented: the CDP port's own `screenshot({ fullPage })` has no timeout slot to
107
119
  * forward it to, and a deadline enforced in `page-over-target.ts` would have to race
108
- * `ScrapeClock.sleep`, which under `testClock` resolves on the first microtask and would time
120
+ * `ScrapeClock.sleep`, which under `testScrapeClock` resolves on the first microtask and would time
109
121
  * out every capture in every test. A driver's own default is the honest bound.
110
122
  */
111
123
  export type CaptureOptions = CaptureFraming;
@@ -134,7 +146,7 @@ export interface ScrapeTarget {
134
146
  goto(url: string, options: GotoOptions): Promise<void>;
135
147
  /** Serialised HTML of THIS target — the document for a page, the subtree for a frame. */
136
148
  content(): Promise<string>;
137
- query(selector: string): Promise<readonly ElementSnapshot[]>;
149
+ query(selector: string, options?: QueryOptions): Promise<readonly ElementSnapshot[]>;
138
150
  /**
139
151
  * Clicks the FIRST match. It took an `index` until 2026-08 that `html-target.ts` honoured and
140
152
  * `cdp-target.ts` dropped — its implementations are `click: (selector) => …`, so puppeteer
@@ -51,13 +51,61 @@ export function splitCredentials(url: string): {
51
51
  return { bare: parsed.toString().replace(/\/$/, ''), username, password };
52
52
  }
53
53
 
54
+ /**
55
+ * The words that name a credential, matched against a query key's COMPONENTS — split on `_`, `-`,
56
+ * `.` and camelCase — never as substrings: `keyboard` holds `key` and `monkey` holds it too, and
57
+ * concealing their values blanked ordinary words out of every artifact. Read off the key and not
58
+ * the value, because a provider's token can be short and the key is the URL's own statement.
59
+ */
60
+ const CREDENTIAL_WORDS = new Set([
61
+ 'token',
62
+ 'key',
63
+ 'apikey',
64
+ 'secret',
65
+ 'auth',
66
+ 'authorization',
67
+ 'sig',
68
+ 'signature',
69
+ 'pass',
70
+ 'password',
71
+ 'passwd',
72
+ 'pwd',
73
+ 'cred',
74
+ 'credential',
75
+ 'credentials',
76
+ 'session',
77
+ 'sessionid',
78
+ 'sid',
79
+ 'jwt',
80
+ 'bearer',
81
+ ]);
82
+
83
+ const credentialKey = (key: string): boolean =>
84
+ key
85
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
86
+ .toLowerCase()
87
+ .split(/[\s_.-]+/)
88
+ .some((part) => CREDENTIAL_WORDS.has(part));
89
+
90
+ /**
91
+ * A value that is a capability whatever it is called: long, one unbroken run, letters and digits
92
+ * mixed — a browser id, an access token. A word (`residential`, `true`, `eu`) is none of those.
93
+ */
94
+ const credentialShaped = (value: string): boolean =>
95
+ value.length >= 16 && !/\s/.test(value) && /[0-9]/.test(value) && /[a-z]/i.test(value);
96
+
54
97
  /**
55
98
  * Every value in the URL that is, or may be, a credential — for the run's redaction set.
56
99
  *
57
- * The whole URL first, then the parts that can be quoted on their own: a launcher's error says
58
- * `401 for /devtools/browser/<id>?token=…` as readily as it repeats the URL. The path counts
59
- * because a browser id in a CDP path IS the capability. Encoded and decoded forms both, since a
60
- * message may carry either. The host is left out on purpose: it is what an error is allowed to say.
100
+ * The whole URL first, then the parts a message quotes on their own: a launcher's error says
101
+ * `401 for /devtools/browser/<id>?token=…` as readily as it repeats the URL. Encoded and decoded
102
+ * forms both, since a message may carry either. The host is left out on purpose: it is what an
103
+ * error is allowed to say.
104
+ *
105
+ * A query VALUE or a path SEGMENT only when it is credential-shaped or sits under a
106
+ * credential-named key — never every value. Concealing `stealth=true&proxy=residential` blanked
107
+ * every `true` and `residential` in the page, the console and the network ring of the run: the
108
+ * unreadable artifact `MIN_REDACTABLE_LENGTH` exists to prevent.
61
109
  */
62
110
  export function urlSecretValues(url: string): readonly string[] {
63
111
  const values = new Set<string>([url]);
@@ -67,9 +115,22 @@ export function urlSecretValues(url: string): readonly string[] {
67
115
  // `admin` by value would blank the word out of every page artifact.
68
116
  values.add(parsed.password);
69
117
  values.add(decoded(parsed.password));
70
- if (parsed.pathname !== '/') values.add(`${parsed.pathname}${parsed.search}`);
71
- if (parsed.search !== '') values.add(parsed.search.slice(1));
72
- for (const value of parsed.searchParams.values()) values.add(value);
118
+ if (parsed.search !== '') {
119
+ values.add(`${parsed.pathname}${parsed.search}`);
120
+ values.add(parsed.search.slice(1));
121
+ }
122
+ const segments = parsed.pathname.split('/');
123
+ for (const [index, segment] of segments.entries()) {
124
+ // By POSITION as well as by shape: Chrome's `/devtools/<browser|page>/<id>` id IS the
125
+ // capability, and an id of letters alone is not credential-shaped.
126
+ const capability = index >= 2 && segments[index - 2] === 'devtools';
127
+ if (capability || credentialShaped(decoded(segment))) {
128
+ values.add(decoded(segment)).add(segment);
129
+ }
130
+ }
131
+ for (const [key, value] of parsed.searchParams) {
132
+ if (credentialKey(key) || credentialShaped(value)) values.add(value);
133
+ }
73
134
  } catch {
74
135
  // Not a URL: the whole string above is all there is to redact.
75
136
  }
package/src/usage.ts CHANGED
@@ -43,7 +43,7 @@ export interface RunUsageMeter extends UsageMeter {
43
43
  }
44
44
 
45
45
  /** Built BEFORE `driver.open()`, so the time a rented browser took to arrive is time it was held. */
46
- export function createUsageMeter(clock: ScrapeClock): RunUsageMeter {
46
+ export function usageMeter(clock: ScrapeClock): RunUsageMeter {
47
47
  const startedAt = clock.monotonic();
48
48
  let navigations = 0;
49
49
  let httpRequests = 0;
package/src/cdp-fake.ts DELETED
@@ -1,420 +0,0 @@
1
- // A CDP-shaped browser with no browser behind it — the seam `driver-parity.test.ts` needs to run
2
- // the SAME suite against the real driver's code path without Chrome.
3
- //
4
- // It exists for one reason, and it is the reason `mock.module('puppeteer-core')` is banned in this
5
- // package: `mock.module` replaces a module for the whole run, `bun test` does not fully serialise
6
- // test files, and a mock installed here has been observed leaking into a concurrently-running
7
- // file's assertions. An INJECTED launcher is a value; a value cannot leak.
8
- //
9
- // Precedent: `packages/storage/src/driver-s3-fixture.ts` ships the same way.
10
-
11
- import type {
12
- CdpBrowserLike,
13
- CdpFrameLike,
14
- CdpLauncherLike,
15
- CdpPageLike,
16
- CdpSessionLike,
17
- } from './cdp-port';
18
- import { COLOR_SCHEME_FEATURE } from './color-scheme';
19
- import { queryHtml } from './html-query';
20
- import type { AxNode, ElementSnapshot, ScrapeCookie } from './target';
21
-
22
- /** The selector inside `snapshotExpression()`'s `document.querySelectorAll("…")`. */
23
- const selectorOf = (expression: string): string | undefined => {
24
- const match = /querySelectorAll\((".*?")\)/s.exec(expression);
25
- if (match?.[1] === undefined) return undefined;
26
- return JSON.parse(match[1]) as string;
27
- };
28
-
29
- /**
30
- * The selector inside `clearExpression()`'s `document.querySelector("…")`, READ rather than
31
- * imported. A clear is the one verb this port has no method for — it travels as an expression — so
32
- * a fake that recognised it by identity could not tell the page's document from a frame's, which
33
- * is precisely the divergence `driver-parity-frames.test.ts` needs it to expose.
34
- */
35
- const clearedSelectorOf = (expression: string): string | undefined => {
36
- if (!expression.includes(".value = ''")) return undefined;
37
- const match = /querySelector\((".*?")\)/s.exec(expression);
38
- if (match?.[1] === undefined) return undefined;
39
- return JSON.parse(match[1]) as string;
40
- };
41
-
42
- export interface FakeCdpFrameInit {
43
- readonly url: string;
44
- readonly html: string;
45
- }
46
-
47
- export interface FakeCdpPageInit {
48
- readonly url: string;
49
- readonly html: string;
50
- /** Selectors a click navigates from, and where to. */
51
- readonly routes?: Readonly<Record<string, { readonly url: string; readonly html: string }>>;
52
- /**
53
- * Child frames by NAME, each its own document. A frame with its own markup is what makes a
54
- * frame verb aimed at the parent document visible — with `frames(): []` the whole frame half of
55
- * `cdp-target.ts` was unreachable from every test in the package.
56
- */
57
- readonly frames?: Readonly<Record<string, FakeCdpFrameInit>>;
58
- readonly cookies?: readonly ScrapeCookie[];
59
- readonly storage?: Readonly<Record<string, string>>;
60
- readonly userAgent?: string;
61
- /** Selectors whose element is covered at its centre — what only a layout engine can see. */
62
- readonly covered?: readonly string[];
63
- /**
64
- * What the accessibility tree answers, BY SELECTOR — the nodes `Accessibility.getPartialAXTree`
65
- * would compute for each match, in document order. Canned rather than derived from the markup,
66
- * deliberately: a fake that read `role=` off the tag would be the offline driver's refusal
67
- * re-implemented as a lie, and the point of this session is to exercise `cdp-a11y.ts`'s four
68
- * protocol calls and its parse, not to compute a role.
69
- */
70
- readonly accessibility?: Readonly<Record<string, readonly AxNode[]>>;
71
- }
72
-
73
- type Handlers = Map<string, ((payload: unknown) => void)[]>;
74
-
75
- export interface FakeCdpBrowser extends CdpBrowserLike {
76
- /** Fire a request event, as a real browser would when the page fetches a subresource. */
77
- emitRequest(url: string, resourceType: string): void;
78
- /**
79
- * Fire `pageerror` — the page threw and nothing caught it. `payload` is whatever the library
80
- * would hand a handler, `unknown` on purpose: a page can throw a string as easily as an `Error`,
81
- * and the target reads it defensively either way.
82
- */
83
- emitPageError(payload: unknown): void;
84
- readonly aborted: readonly string[];
85
- readonly closed: boolean;
86
- /** What `page.setOfflineMode()` last set, so a test asserts on the CONDITION, not on a call. */
87
- readonly offline: boolean;
88
- /**
89
- * What `page.emulateMediaFeatures()` last set `prefers-color-scheme` to, for `offline`'s reason
90
- * — the condition the page is now in, never the fact that a method was called. `null` until
91
- * something sets one, which is the launcher's own default and not a value this fake invents.
92
- */
93
- readonly colorScheme: string | null;
94
- /**
95
- * Every keyboard event, in order — `down Meta`, `press K`, `up Meta` — so a test asserts on the
96
- * SEQUENCE a chord became: a modifier released before the key, or never released, is the defect.
97
- */
98
- readonly pressed: readonly string[];
99
- /** Every selector `page.focus()` was handed, in order. */
100
- readonly focused: readonly string[];
101
- /**
102
- * Every expression `page.evaluateOnNewDocument()` was handed, in order — the scripts a real
103
- * browser would run ahead of each document's own. Recorded, never executed: this fake has no JS
104
- * engine, and a test asserts on WHAT was prepared and that it was prepared before `goto`.
105
- */
106
- readonly prepared: readonly string[];
107
- /** How many raw CDP sessions were created and how many detached — a leak is a difference. */
108
- readonly sessions: { readonly created: number; readonly detached: number };
109
- }
110
-
111
- /** A layout box every element gets, so the CDP path exercises the fields the fake target lacks. */
112
- const boxed = (snapshot: ElementSnapshot, covered: boolean): ElementSnapshot => ({
113
- ...snapshot,
114
- box: { x: 10, y: 10, width: 100, height: 20 },
115
- hitTarget: !covered,
116
- });
117
-
118
- /**
119
- * ONE document — the page's, or a frame's. A browser gives every browsing context its own DOM, so
120
- * the fake gives every one its own typed values: a verb that reaches the wrong document then shows
121
- * up as text in the wrong place rather than as nothing at all.
122
- */
123
- interface FakeDocument {
124
- html(): string;
125
- /** Typed text, keyed the way `keyOf` keys it offline: `#id` when there is one, else the selector. */
126
- readonly typed: Map<string, string>;
127
- }
128
-
129
- const keyFor = (selector: string, element: ElementSnapshot | undefined): string => {
130
- const id = element?.attrs['id'];
131
- return id === undefined ? selector : `#${id}`;
132
- };
133
-
134
- /** What the DOM would answer after a `type()`: the element, carrying whatever was typed into it. */
135
- const withTyped = (
136
- document: FakeDocument,
137
- selector: string,
138
- elements: readonly ElementSnapshot[],
139
- ): readonly ElementSnapshot[] =>
140
- elements.map((element) => {
141
- const typed = document.typed.get(keyFor(selector, element));
142
- return typed === undefined ? element : { ...element, value: typed };
143
- });
144
-
145
- export function fakeCdpBrowser(init: FakeCdpPageInit): FakeCdpBrowser {
146
- const handlers: Handlers = new Map();
147
- const aborted: string[] = [];
148
- let url = init.url;
149
- let html = init.html;
150
- let closed = false;
151
- let offline = false;
152
- let colorScheme: string | null = null;
153
- const covered = new Set(init.covered ?? []);
154
- const storage: Record<string, string> = { ...init.storage };
155
- let cookies: readonly ScrapeCookie[] = init.cookies ?? [];
156
- const pressed: string[] = [];
157
- const focused: string[] = [];
158
- const prepared: string[] = [];
159
- const sessions = { created: 0, detached: 0 };
160
- const axBySelector = init.accessibility ?? {};
161
-
162
- /**
163
- * A raw session answering the four commands `cdp-a11y.ts` sends, in CDP's own wire shapes —
164
- * `{ value }` wrappers, a `properties` list — so the parse in that file is what is under test.
165
- * Node ids are minted per `querySelectorAll` and looked up on the way back, which is how the
166
- * real protocol addresses nodes too.
167
- */
168
- const cdpSession = (): CdpSessionLike => {
169
- const byNodeId = new Map<number, AxNode>();
170
- const wrap = (value: string | boolean | undefined): { value?: string | boolean } =>
171
- value === undefined ? {} : { value };
172
- return {
173
- send: (method: string, params?: Record<string, unknown>) => {
174
- if (method === 'DOM.getDocument') return Promise.resolve({ root: { nodeId: 1 } });
175
- if (method === 'DOM.querySelectorAll') {
176
- const selector = typeof params?.['selector'] === 'string' ? params['selector'] : '';
177
- const nodes = Object.hasOwn(axBySelector, selector) ? (axBySelector[selector] ?? []) : [];
178
- const nodeIds = nodes.map((node, index) => {
179
- const nodeId = 100 + byNodeId.size + index;
180
- byNodeId.set(nodeId, node);
181
- return nodeId;
182
- });
183
- return Promise.resolve({ nodeIds });
184
- }
185
- if (method === 'DOM.describeNode') {
186
- const nodeId = typeof params?.['nodeId'] === 'number' ? params['nodeId'] : 0;
187
- return Promise.resolve({ node: { backendNodeId: nodeId * 10 } });
188
- }
189
- if (method === 'Accessibility.getPartialAXTree') {
190
- const backend =
191
- typeof params?.['backendNodeId'] === 'number' ? params['backendNodeId'] : 0;
192
- const node = byNodeId.get(backend / 10);
193
- if (node === undefined) return Promise.resolve({ nodes: [] });
194
- const properties = [
195
- ...(node.focused === undefined ? [] : [{ name: 'focused', value: wrap(node.focused) }]),
196
- ...(node.disabled === undefined
197
- ? []
198
- : [{ name: 'disabled', value: wrap(node.disabled) }]),
199
- ];
200
- return Promise.resolve({
201
- nodes: [
202
- {
203
- nodeId: `ax-${String(backend)}`,
204
- ignored: node.ignored,
205
- role: { type: 'role', ...wrap(node.role) },
206
- name: { type: 'computedString', ...wrap(node.name) },
207
- ...(node.description === undefined
208
- ? {}
209
- : { description: { type: 'computedString', value: node.description } }),
210
- ...(node.value === undefined
211
- ? {}
212
- : { value: { type: 'string', value: node.value } }),
213
- properties,
214
- },
215
- ],
216
- });
217
- }
218
- return Promise.resolve({});
219
- },
220
- detach: () => {
221
- sessions.detached += 1;
222
- return Promise.resolve();
223
- },
224
- };
225
- };
226
-
227
- const documentOf = (read: () => string): FakeDocument => ({ html: read, typed: new Map() });
228
- const pageDocument = documentOf(() => html);
229
-
230
- /** Every expression this package sends, answered against the document it was sent to. */
231
- const evaluateIn = async (target: FakeDocument, expression: string): Promise<unknown> => {
232
- const selector = selectorOf(expression);
233
- if (selector !== undefined) {
234
- const found = withTyped(target, selector, await queryHtml(target.html(), selector));
235
- return JSON.stringify(found.map((element) => boxed(element, covered.has(selector))));
236
- }
237
- const cleared = clearedSelectorOf(expression);
238
- if (cleared !== undefined) {
239
- const [first] = await queryHtml(target.html(), cleared);
240
- target.typed.set(keyFor(cleared, first), '');
241
- return undefined;
242
- }
243
- return undefined;
244
- };
245
-
246
- const typeIn = async (target: FakeDocument, selector: string, text: string): Promise<void> => {
247
- const [first] = await queryHtml(target.html(), selector);
248
- const key = keyFor(selector, first);
249
- target.typed.set(key, `${target.typed.get(key) ?? first?.value ?? ''}${text}`);
250
- };
251
-
252
- const selectIn = async (target: FakeDocument, selector: string, value: string): Promise<void> => {
253
- const [first] = await queryHtml(target.html(), selector);
254
- target.typed.set(keyFor(selector, first), value);
255
- };
256
-
257
- const frames: readonly CdpFrameLike[] = Object.entries(init.frames ?? {}).map(([name, frame]) => {
258
- const frameDocument = documentOf(() => frame.html);
259
- return {
260
- name: () => name,
261
- url: () => frame.url,
262
- content: () => Promise.resolve(frame.html),
263
- evaluate: (expression: string) => evaluateIn(frameDocument, expression),
264
- click: () => Promise.resolve(),
265
- type: (selector: string, text: string) => typeIn(frameDocument, selector, text),
266
- select: async (selector: string, ...values: string[]): Promise<string[]> => {
267
- await selectIn(frameDocument, selector, values[0] ?? '');
268
- return [...values];
269
- },
270
- };
271
- });
272
-
273
- const page: CdpPageLike = {
274
- url: () => url,
275
- goto: (next: string) => {
276
- url = next;
277
- // A navigation is a new document, so what was typed into the old one is gone. Keeping it
278
- // would let a stale value answer a query on a page that never had it.
279
- pageDocument.typed.clear();
280
- return Promise.resolve(undefined);
281
- },
282
- content: () => Promise.resolve(html),
283
- async evaluate(expression: string): Promise<unknown> {
284
- if (expression.includes('localStorage')) {
285
- if (expression.includes('setItem')) return undefined;
286
- return JSON.stringify(storage);
287
- }
288
- if (expression.includes('navigator.userAgent')) return init.userAgent ?? 'fake-agent';
289
- return await evaluateIn(pageDocument, expression);
290
- },
291
- click: (selector: string) => {
292
- const route = init.routes?.[selector];
293
- if (route !== undefined) {
294
- url = route.url;
295
- html = route.html;
296
- pageDocument.typed.clear();
297
- }
298
- return Promise.resolve();
299
- },
300
- type: (selector: string, text: string) => typeIn(pageDocument, selector, text),
301
- select: async (selector: string, ...values: string[]): Promise<string[]> => {
302
- await selectIn(pageDocument, selector, values[0] ?? '');
303
- return [...values];
304
- },
305
- screenshot: () => Promise.resolve(new Uint8Array([1, 2, 3])),
306
- pdf: () => Promise.resolve(new Uint8Array([4, 5])),
307
- setRequestInterception: () => Promise.resolve(),
308
- keyboard: {
309
- down: (key: string) => {
310
- pressed.push(`down ${key}`);
311
- return Promise.resolve();
312
- },
313
- up: (key: string) => {
314
- pressed.push(`up ${key}`);
315
- return Promise.resolve();
316
- },
317
- press: (key: string) => {
318
- pressed.push(`press ${key}`);
319
- return Promise.resolve();
320
- },
321
- },
322
- focus: (selector: string) => {
323
- focused.push(selector);
324
- return Promise.resolve();
325
- },
326
- evaluateOnNewDocument: (expression: string) => {
327
- prepared.push(expression);
328
- return Promise.resolve(undefined);
329
- },
330
- createCDPSession: () => {
331
- sessions.created += 1;
332
- return Promise.resolve(cdpSession());
333
- },
334
- setOfflineMode: (enabled: boolean) => {
335
- offline = enabled;
336
- return Promise.resolve();
337
- },
338
- // CDP's own two rules, both of them. An EMPTY list is a RESET — measured on Chrome 150, after
339
- // `emulateMediaFeatures([])` the page answers exactly what an untouched one does — so it must
340
- // clear here rather than be ignored, or `'no-preference'` records the previous scheme forever.
341
- // And a non-empty list is read by NAME, never `features[0].value`: the port's shape is a list,
342
- // and a caller setting `prefers-reduced-motion` beside the scheme must not overwrite it.
343
- emulateMediaFeatures: (features: readonly { name: string; value: string }[]) => {
344
- if (features.length === 0) {
345
- colorScheme = null;
346
- return Promise.resolve();
347
- }
348
- const found = features.find((feature) => feature.name === COLOR_SCHEME_FEATURE);
349
- if (found !== undefined) colorScheme = found.value;
350
- return Promise.resolve();
351
- },
352
- on: (event: string, handler: (payload: unknown) => void) => {
353
- const listeners = handlers.get(event) ?? [];
354
- listeners.push(handler);
355
- handlers.set(event, listeners);
356
- return undefined;
357
- },
358
- frames: (): readonly CdpFrameLike[] => frames,
359
- close: () => {
360
- closed = true;
361
- return Promise.resolve();
362
- },
363
- };
364
-
365
- return {
366
- newPage: () => Promise.resolve(page),
367
- cookies: () => Promise.resolve(cookies),
368
- setCookie: (...next: readonly unknown[]) => {
369
- cookies = next as readonly ScrapeCookie[];
370
- return Promise.resolve();
371
- },
372
- close: () => {
373
- closed = true;
374
- return Promise.resolve();
375
- },
376
- process: () => null,
377
- emitRequest(requestUrl: string, resourceType: string): void {
378
- for (const handler of handlers.get('request') ?? []) {
379
- handler({
380
- url: () => requestUrl,
381
- resourceType: () => resourceType,
382
- abort: () => {
383
- aborted.push(requestUrl);
384
- return Promise.resolve();
385
- },
386
- continue: () => Promise.resolve(),
387
- });
388
- }
389
- },
390
- emitPageError(payload: unknown): void {
391
- for (const handler of handlers.get('pageerror') ?? []) handler(payload);
392
- },
393
- aborted,
394
- get closed(): boolean {
395
- return closed;
396
- },
397
- get offline(): boolean {
398
- return offline;
399
- },
400
- get colorScheme(): string | null {
401
- return colorScheme;
402
- },
403
- pressed,
404
- focused,
405
- prepared,
406
- sessions,
407
- };
408
- }
409
-
410
- /** A launcher over `fakeCdpBrowser`, for `localBrowser({ launcher })` in a test. */
411
- export function fakeCdpLauncher(init: FakeCdpPageInit): CdpLauncherLike & {
412
- readonly browser: FakeCdpBrowser;
413
- } {
414
- const browser = fakeCdpBrowser(init);
415
- return {
416
- browser,
417
- launch: () => Promise.resolve(browser),
418
- connect: () => Promise.resolve(browser),
419
- };
420
- }
package/src/hosts.ts DELETED
@@ -1,4 +0,0 @@
1
- // `allowHosts` matching is `@ultimat3/core`'s (`host-rules.ts`), shared with `x shot`'s browser;
2
- // re-exported here so this package's own modules and its public names are unchanged.
3
- export type { HostDecision, HostRule } from '@ultimat3/core';
4
- export { ANY_HOST, hostDecision, hostMatches } from '@ultimat3/core';