@unotest/web 0.5.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.
@@ -0,0 +1,407 @@
1
+ import { Matcher } from '@unotest/protocol';
2
+ import { Browser, BrowserChannel } from './config/schema.js';
3
+
4
+ type LocatorStep = {
5
+ kind: 'css';
6
+ selector: string;
7
+ } | {
8
+ kind: 'getByRole';
9
+ role: string;
10
+ name?: Matcher;
11
+ exact?: boolean;
12
+ } | {
13
+ kind: 'getByText';
14
+ text: Matcher;
15
+ exact?: boolean;
16
+ } | {
17
+ kind: 'getByLabel';
18
+ text: Matcher;
19
+ exact?: boolean;
20
+ } | {
21
+ kind: 'getByPlaceholder';
22
+ text: Matcher;
23
+ exact?: boolean;
24
+ } | {
25
+ kind: 'getByTestId';
26
+ id: string;
27
+ } | {
28
+ kind: 'getByAltText';
29
+ text: Matcher;
30
+ exact?: boolean;
31
+ } | {
32
+ kind: 'getByTitle';
33
+ text: Matcher;
34
+ exact?: boolean;
35
+ } | {
36
+ kind: 'filter';
37
+ hasText?: Matcher;
38
+ hasNotText?: Matcher;
39
+ has?: LocatorValue;
40
+ hasNot?: LocatorValue;
41
+ } | {
42
+ kind: 'first';
43
+ } | {
44
+ kind: 'last';
45
+ } | {
46
+ kind: 'nth';
47
+ index: number;
48
+ } | {
49
+ kind: 'contentFrame';
50
+ } | {
51
+ kind: 'ref';
52
+ ref: string;
53
+ };
54
+ interface LocatorValue {
55
+ readonly kind: 'locator';
56
+ readonly steps: readonly LocatorStep[];
57
+ }
58
+ declare function isLocatorValue(value: unknown): value is LocatorValue;
59
+ /** Internal: build a new LocatorValue by appending one step. Immutable. */
60
+ declare function appendStep(loc: LocatorValue, step: LocatorStep): LocatorValue;
61
+ /** Internal: construct a fresh LocatorValue rooted at one step. */
62
+ declare function rootedAt(step: LocatorStep): LocatorValue;
63
+ /** Debug-friendly string repr. NOT a selector — for logs / errors only. */
64
+ declare function describeLocator(loc: LocatorValue): string;
65
+
66
+ interface LaunchOptions {
67
+ browser: Browser;
68
+ /** Playwright channel — `'chrome'` / `'msedge'` to use a system-
69
+ * installed browser, `null`/undefined to fall back to bundled
70
+ * Chromium. Only meaningful when `browser === 'chromium'`. */
71
+ channel?: BrowserChannel;
72
+ /** Run with visible window. Default: false in CI, true in dev. */
73
+ headless?: boolean;
74
+ /** Slow each operation by N ms — for dev debugging. Default: 0. */
75
+ slowMo?: number;
76
+ }
77
+ interface Viewport {
78
+ width: number;
79
+ height: number;
80
+ }
81
+ interface ContextOptions {
82
+ baseUrl?: string;
83
+ viewport?: Viewport;
84
+ /** Path to storageState JSON (D-23 caching, optional). */
85
+ storageState?: string;
86
+ /** Per-context override for dialog policy. Defaults to config.dialogPolicy. */
87
+ dialogPolicy?: "accept" | "dismiss" | "manual";
88
+ /** User-Agent string override. */
89
+ userAgent?: string;
90
+ /** Locale, e.g. "en-US". */
91
+ locale?: string;
92
+ /** Timezone, e.g. "Europe/Moscow". */
93
+ timezoneId?: string;
94
+ /**
95
+ * Default action timeout in milliseconds — applied to every Playwright
96
+ * `click` / `fill` / `waitFor` / etc. via `context.setDefaultTimeout`.
97
+ * Single source of truth lives in `UnotestConfigSchema.defaultTimeoutMs`
98
+ * (default 3000ms); `UNOTEST_DEFAULT_TIMEOUT_MS` env overrides.
99
+ */
100
+ defaultTimeoutMs?: number;
101
+ /**
102
+ * Default navigation timeout in milliseconds — applied to `goto` /
103
+ * `reload` / `waitForUrl` via `context.setDefaultNavigationTimeout`.
104
+ * Action timeout (3s) is too aggressive for real-world page loads;
105
+ * keep navigation at Playwright stock 30000ms by default.
106
+ * Single source of truth lives in `UnotestConfigSchema.defaultNavigationTimeoutMs`;
107
+ * `UNOTEST_DEFAULT_NAVIGATION_TIMEOUT_MS` env overrides.
108
+ */
109
+ defaultNavigationTimeoutMs?: number;
110
+ }
111
+ interface ActionTimeout {
112
+ /** Override default action timeout (D-7 actionability wait). Milliseconds. */
113
+ timeout?: number;
114
+ }
115
+ interface ClickOptions extends ActionTimeout {
116
+ /** Bypass actionability checks (D-5 — replaces testoid5 `forceClick`). */
117
+ force?: boolean;
118
+ /** Click at specific offset within element. */
119
+ position?: {
120
+ x: number;
121
+ y: number;
122
+ };
123
+ /** Hold these modifiers during click. */
124
+ modifiers?: ReadonlyArray<"Alt" | "Control" | "Meta" | "Shift">;
125
+ /** Mouse button. Default: 'left'. */
126
+ button?: "left" | "right" | "middle";
127
+ /** Number of clicks. Default: 1. */
128
+ clickCount?: number;
129
+ }
130
+ interface FillOptions extends ActionTimeout {
131
+ force?: boolean;
132
+ }
133
+ interface PressOptions extends ActionTimeout {
134
+ /** Delay between keydown and keyup, in ms. */
135
+ delay?: number;
136
+ }
137
+ interface CheckOptions extends ActionTimeout {
138
+ force?: boolean;
139
+ }
140
+ interface SelectOptions extends ActionTimeout {
141
+ force?: boolean;
142
+ }
143
+ interface HoverOptions extends ActionTimeout {
144
+ force?: boolean;
145
+ position?: {
146
+ x: number;
147
+ y: number;
148
+ };
149
+ }
150
+ /**
151
+ * State to wait for. Default: 'visible'.
152
+ * - 'attached' — element exists in DOM
153
+ * - 'detached' — element removed from DOM
154
+ * - 'visible' — exists + visible (non-zero size, not hidden)
155
+ * - 'hidden' — exists with zero size OR not in DOM
156
+ */
157
+ type WaitState = "attached" | "detached" | "visible" | "hidden";
158
+ interface WaitOptions extends ActionTimeout {
159
+ state?: WaitState;
160
+ }
161
+ interface NavigationOptions extends ActionTimeout {
162
+ /**
163
+ * When to consider navigation done. Default: 'load'.
164
+ * - 'load' — `load` event fired
165
+ * - 'domcontentloaded' — `DOMContentLoaded` fired
166
+ * - 'networkidle' — no network requests for 500ms
167
+ * - 'commit' — initial response received
168
+ */
169
+ waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
170
+ }
171
+ type GotoOptions = NavigationOptions;
172
+ type ReloadOptions = NavigationOptions;
173
+ interface ScreenshotOptions {
174
+ /** Capture full page (scroll), not just viewport. Default: false. */
175
+ fullPage?: boolean;
176
+ /** Crop to a specific element. */
177
+ clip?: {
178
+ x: number;
179
+ y: number;
180
+ width: number;
181
+ height: number;
182
+ };
183
+ /** PNG quality (1-100). Only honoured for JPEG. */
184
+ quality?: number;
185
+ }
186
+ interface CookieOptions {
187
+ domain?: string;
188
+ path?: string;
189
+ expires?: number;
190
+ httpOnly?: boolean;
191
+ secure?: boolean;
192
+ sameSite?: "Strict" | "Lax" | "None";
193
+ }
194
+
195
+ interface WebDriver {
196
+ /**
197
+ * Launch the underlying browser process. Idempotent — subsequent calls
198
+ * resolve immediately if already launched. Must be called before
199
+ * newContext().
200
+ */
201
+ launch(opts: LaunchOptions): Promise<void>;
202
+ /**
203
+ * Create a fresh context (D-23). Cookies / localStorage / sessionStorage
204
+ * / tabs / frames start clean. Per-test isolation lives at this layer —
205
+ * each test calls newContext() at start and ctx.close() at end.
206
+ */
207
+ newContext(opts?: ContextOptions): Promise<DriverContext>;
208
+ /**
209
+ * Tear down all open contexts and the browser process. Idempotent.
210
+ * Errors logged but not re-thrown (must not mask test failures in
211
+ * `finally` blocks).
212
+ */
213
+ close(): Promise<void>;
214
+ }
215
+ interface DriverContext {
216
+ /** Currently-open tabs/windows in this context. Index 0 = first opened. */
217
+ pages(): ReadonlyArray<DriverPage>;
218
+ /** The page that subsequent operations target. */
219
+ activePage(): DriverPage;
220
+ /** Switch the active page index (D-4 setPage). 0 = first opened, etc.
221
+ * Implementations MUST also reset the frame stack — a frame scope
222
+ * established on tab A is not valid on tab B. */
223
+ setActivePage(index: number): Promise<void>;
224
+ /**
225
+ * Subscribe to new-page events. Listener fires when a tab/window opens
226
+ * in this context (`target="_blank"`, `window.open`, etc.). Returns an
227
+ * unsubscribe function — call it to detach the listener. Cross-browser:
228
+ * Playwright abstracts the underlying CDP / Juggler / WebKit signal so
229
+ * this works in Chromium, Firefox, and WebKit identically.
230
+ *
231
+ * Used by the explore tool's popup-watcher; tests can fan out
232
+ * synchronously into a buffer.
233
+ */
234
+ onNewPage(listener: (info: {
235
+ index: number;
236
+ url: string;
237
+ }) => void): () => void;
238
+ /**
239
+ * Enter a frame scope (D-4 stateful pair). All subsequent locators
240
+ * resolve inside this frame until `exitFrame()` is called. Stacks (calling
241
+ * enterFrame twice nests, exitFrame pops one level).
242
+ */
243
+ enterFrame(locator: LocatorValue): Promise<void>;
244
+ exitFrame(): Promise<void>;
245
+ /** Current frame-stack depth — 0 when no `enterFrame` is active. Used
246
+ * by the popup-watcher to know how many `exit_frame` entries to emit
247
+ * before auto-switching to a popup (frame scope does not survive tab
248
+ * changes). */
249
+ frameDepth(): number;
250
+ /** Typed storage shortcuts (D-6). */
251
+ setLocalStorage(key: string, value: string): Promise<void>;
252
+ getLocalStorage(key: string): Promise<string | null>;
253
+ setCookie(name: string, value: string, opts?: CookieOptions): Promise<void>;
254
+ getCookie(name: string): Promise<string | null>;
255
+ /**
256
+ * Persist current storage state to disk (D-23 opt-in caching). Used by
257
+ * project helpers like loginAndCacheState().
258
+ */
259
+ saveStorageState(path: string): Promise<void>;
260
+ /** Run JS in the page context (D-6 escape hatch). */
261
+ evaluate<T = unknown>(js: string): Promise<T>;
262
+ /**
263
+ * Snapshot of console entries captured since context creation, oldest
264
+ * first. Used by failure-bundle capture (D-16 tier 1). The driver wires
265
+ * a `page.on('console', …)` listener at newContext() time so the
266
+ * runner can pull the tail when a test fails.
267
+ */
268
+ consoleEntries(): ReadonlyArray<{
269
+ level: "log" | "info" | "warn" | "error" | "debug";
270
+ text: string;
271
+ timestamp: string;
272
+ }>;
273
+ /** Close all pages in this context. Idempotent. */
274
+ close(): Promise<void>;
275
+ }
276
+ interface DriverPage {
277
+ /** Current URL. Synchronous — reads cached state. */
278
+ url(): string;
279
+ /** Current document title. */
280
+ title(): Promise<string>;
281
+ click(loc: LocatorValue, opts?: ClickOptions): Promise<void>;
282
+ doubleClick(loc: LocatorValue, opts?: ClickOptions): Promise<void>;
283
+ fill(loc: LocatorValue, value: string, opts?: FillOptions): Promise<void>;
284
+ press(loc: LocatorValue, key: string, opts?: PressOptions): Promise<void>;
285
+ check(loc: LocatorValue, opts?: CheckOptions): Promise<void>;
286
+ uncheck(loc: LocatorValue, opts?: CheckOptions): Promise<void>;
287
+ selectOption(loc: LocatorValue, value: string | ReadonlyArray<string>, opts?: SelectOptions): Promise<void>;
288
+ hover(loc: LocatorValue, opts?: HoverOptions): Promise<void>;
289
+ scrollIntoView(loc: LocatorValue): Promise<void>;
290
+ /** Drag the source element onto the target. */
291
+ dragAndDrop(from: LocatorValue, to: LocatorValue, opts?: ClickOptions): Promise<void>;
292
+ /** Set the input element's selected files. Accepts one or many paths. */
293
+ uploadFile(loc: LocatorValue, files: string | ReadonlyArray<string>): Promise<void>;
294
+ /** Focus the locator then dispatch a paste-like input with `text`. */
295
+ clipboardPaste(loc: LocatorValue, text: string): Promise<void>;
296
+ count(loc: LocatorValue): Promise<number>;
297
+ textContent(loc: LocatorValue): Promise<string | null>;
298
+ inputValue(loc: LocatorValue): Promise<string>;
299
+ isVisible(loc: LocatorValue): Promise<boolean>;
300
+ /** Read a DOM attribute on the matched element. Returns null if absent. */
301
+ getAttribute(loc: LocatorValue, name: string): Promise<string | null>;
302
+ waitFor(loc: LocatorValue, opts?: WaitOptions): Promise<void>;
303
+ waitForUrl(pattern: string | RegExp, opts?: NavigationOptions): Promise<void>;
304
+ waitForText(text: string, opts?: WaitOptions): Promise<void>;
305
+ waitForNavigation(opts?: NavigationOptions): Promise<void>;
306
+ goto(url: string, opts?: GotoOptions): Promise<void>;
307
+ reload(opts?: ReloadOptions): Promise<void>;
308
+ goBack(opts?: NavigationOptions): Promise<void>;
309
+ goForward(opts?: NavigationOptions): Promise<void>;
310
+ evaluate<T = unknown>(js: string): Promise<T>;
311
+ evaluateOnLocator<T = unknown>(loc: LocatorValue, js: string): Promise<T>;
312
+ screenshot(opts?: ScreenshotOptions): Promise<Buffer>;
313
+ inspectElement(loc: LocatorValue): Promise<ElementHint | null>;
314
+ /** outerHTML of the element the locator matches (first match), or null
315
+ * when it resolves to zero elements. Used by debug logging to persist
316
+ * "what did this locator actually resolve to" as agent-readable proof —
317
+ * NOT part of the DSL / action surface. */
318
+ outerHTML(loc: LocatorValue): Promise<string | null>;
319
+ }
320
+ /**
321
+ * Identifying attributes of a single DOM element. Populated by
322
+ * `DriverPage.inspectElement`. Consumers (RefResolver) pick the highest-
323
+ * priority signal to synthesise a stable LocatorValue. All fields are
324
+ * optional — an element might have only a role+name, or only a testId,
325
+ * or nothing stable at all (in which case the resolver throws).
326
+ */
327
+ interface ElementHint {
328
+ /** The wire-only ref the resolver is acting on. Echoed back so the
329
+ * resolver can verify the resolved locator still points at the same
330
+ * element. */
331
+ ref: string;
332
+ /** data-testid / data-qa value, if present. Highest-priority signal. */
333
+ testId?: string;
334
+ /** Computed ARIA role (Playwright's locator role inference). */
335
+ role?: string;
336
+ /** Computed accessible name (W3C accname algorithm). */
337
+ name?: string;
338
+ /** aria-label attribute when the accessible name path is not desired
339
+ * (e.g. icon-only buttons where role+name would still match). */
340
+ ariaLabel?: string;
341
+ /** placeholder attribute on `<input>`/`<textarea>`. Crucial for form
342
+ * fields that lack `<label>` or `aria-label` (common pattern in
343
+ * non-English UIs where placeholder doubles as the visible label).
344
+ * Resolver emits `getByPlaceholder(text)`. */
345
+ placeholder?: string;
346
+ /** `alt` on `<img>` / `<area>` / `<input type="image">`. Resolver
347
+ * emits `getByAltText(text)`. */
348
+ alt?: string;
349
+ /** `title` attribute — tooltip fallback for icon-only buttons / links
350
+ * that have no other label. Named `titleAttr` to avoid collision
351
+ * with `page.title()`. Resolver emits `getByTitle(text)`. */
352
+ titleAttr?: string;
353
+ /** textContent (trimmed, clipped). Last-resort semantic fallback
354
+ * before the CSS escape hatch. */
355
+ text?: string;
356
+ /** href attribute for `<a>` elements lacking any of the above.
357
+ * Resolver emits `locator('a[href="..."]')`. */
358
+ href?: string;
359
+ /** `id` attribute when it looks stable (not framework-generated).
360
+ * Heuristic filters out `:r0:`, `__next_xxx`, long random-hash
361
+ * patterns. Resolver emits `locator('#id')` as a CSS fallback. */
362
+ elementId?: string;
363
+ /** `name` attribute on form fields — stable selector for inputs
364
+ * without testId/label/placeholder. Resolver emits
365
+ * `locator('[name="..."]')`. */
366
+ nameAttr?: string;
367
+ /** A stable, app-authored `data-*` identifier (e.g. `data-id`, `data-guid`,
368
+ * `data-row-id`) — NOT framework noise (`data-v-*`, `data-reactid`) and NOT
369
+ * our own `data-unotest-*`. The single highest-value fallback for grid rows
370
+ * / custom widgets that carry no role+name: the resolver emits
371
+ * `locator('[data-x="..."]')`, turning an otherwise-unaddressable element
372
+ * into a durable locator. Detected generically (name ends in id/guid/uid/
373
+ * key/code, value looks stable). */
374
+ dataAttr?: {
375
+ name: string;
376
+ value: string;
377
+ };
378
+ /** A custom tooltip/label `data-*` attribute (name ends in `-title` /
379
+ * `-tooltip` / `-label`: `data-shadow-title`, `data-original-title`,
380
+ * `data-tooltip`, …). Carries the visible label of icon-only controls
381
+ * that the W3C accname can't see. Generic — matched by suffix, not by a
382
+ * hardcoded attribute name. The resolver emits `locator('[data-x="…"]')`. */
383
+ tooltipAttr?: {
384
+ name: string;
385
+ value: string;
386
+ };
387
+ /** Identifying signals of the element's ancestors (nearest first, ≤6) used
388
+ * to DISAMBIGUATE when the target's own locator is ambiguous: the resolver
389
+ * anchors to the nearest uniquely-locatable ancestor and scopes the
390
+ * candidate within it (`ancestor.locator(candidate)`) — the robust
391
+ * alternative to a positional `nth` (Playwright codegen / Robula+ approach). */
392
+ ancestors?: AncestorHint[];
393
+ }
394
+ /** Compact, cheap-to-compute identifying signals of an ancestor — enough to
395
+ * synthesise a scope anchor. No accname/role (too costly per ancestor). */
396
+ interface AncestorHint {
397
+ testId?: string;
398
+ dataAttr?: {
399
+ name: string;
400
+ value: string;
401
+ };
402
+ elementId?: string;
403
+ /** A single stable, semantic class token (not a hash / utility class). */
404
+ className?: string;
405
+ }
406
+
407
+ export { type ActionTimeout as A, type ContextOptions as C, type DriverContext as D, type ElementHint as E, type FillOptions as F, type GotoOptions as G, type HoverOptions as H, type LaunchOptions as L, type NavigationOptions as N, type PressOptions as P, type ReloadOptions as R, type ScreenshotOptions as S, type Viewport as V, type WebDriver as W, type CheckOptions as a, type ClickOptions as b, type CookieOptions as c, type DriverPage as d, type SelectOptions as e, type WaitOptions as f, type WaitState as g, type LocatorValue as h, type LocatorStep as i, appendStep as j, describeLocator as k, isLocatorValue as l, rootedAt as r };
@@ -0,0 +1,2 @@
1
+
2
+ export { }