@razohq/razo 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mitchell Lombardo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # razo
2
+
3
+ **Playwright UI controls that narrate every action twice: a readable sentence in the report, and a structured JSON artifact built for AI failure analysis.**
4
+
5
+ Page objects have existed for years. razo turns every test into a structured narration — actions and assertions with human names — designed so an AI can analyze failures without guessing. **The output is the product, not the wrapper.**
6
+
7
+ When a test fails, you don't get a bare `locator timed out`. You get *which business action* failed and *what was expected*, both as a readable story in the Playwright HTML report and as machine-readable JSON:
8
+
9
+ ```json
10
+ {
11
+ "test": "export a model without choosing quality",
12
+ "status": "failed",
13
+ "steps": [
14
+ { "action": "fill", "controlType": "field", "name": "Filename", "detail": "mi-llavero", "status": "passed" },
15
+ { "action": "choose", "controlType": "select", "name": "Format", "detail": "3MF", "status": "passed" },
16
+ { "action": "click", "controlType": "button", "name": "Export", "status": "passed" },
17
+ { "action": "assert-text", "controlType": "label", "name": "Export status",
18
+ "expected": "Exported mi-llavero.3mf (Standard)", "actual": "Error: missing fields",
19
+ "status": "failed", "error": "expect(locator).toHaveText(expected) failed ..." }
20
+ ]
21
+ }
22
+ ```
23
+
24
+ A model reading this can answer: *"the export never happened — a filename was typed and a format chosen, but nobody selected the quality, and the app responded 'Error: missing fields'"*.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npm install -D @razohq/razo @playwright/test
30
+ ```
31
+
32
+ ## Quickstart
33
+
34
+ Every control takes a `data-testid` and a **human name**. Every method emits a `test.step()` sentence plus a structured `StepEvent`:
35
+
36
+ ```ts
37
+ import { Button, Input, Select, Label } from '@razohq/razo';
38
+
39
+ const filename = new Input(page, 'filename', 'Filename');
40
+ const format = new Select(page, 'format', 'Format');
41
+ const exportBtn = new Button(page, 'export', 'Export');
42
+ const status = new Label(page, 'status', 'Export status');
43
+
44
+ await filename.fill('mi-llavero'); // Type "mi-llavero" into field "Filename"
45
+ await format.choose('3MF'); // Choose "3MF" in select "Format"
46
+ await exportBtn.click({ as: 'Confirm the export' }); // business-level override
47
+ await status.expectText('Exported mi-llavero.3mf (Standard)');
48
+ ```
49
+
50
+ Controls: `Button`, `Input`, `TextArea`, `RadioButton`, `Checkbox`, `Select` (single and `chooseMany` for multiple), `Slider`, `FileInput`, `Link`, `Label` (assertions only), `Switch`, `Combobox`, `Table`, `Dialog`, `Tabs`, `Image`, `DatePicker`, `Menu`, `Tooltip` (via `aria-describedby`), and `Editor` (contenteditable). Every control requires a name — an anonymous control breaks the narration, so the constructor rejects it.
51
+
52
+ Controls can be scoped inside another control with `{ within }` — the narration stays business-level while the `StepEvent` selector carries the full chain:
53
+
54
+ ```ts
55
+ const dialog = new Dialog(page, 'confirm-dialog', 'Confirm export');
56
+ const yes = new Button(page, 'confirm-yes', 'Yes, export', { within: dialog });
57
+ // StepEvent selector: [data-testid="confirm-dialog"] [data-testid="confirm-yes"]
58
+ ```
59
+
60
+ Universal interactions on every control: `hover`, `doubleClick`, `rightClick`, `press(key)`, `focus`, `scrollTo`, and `dragTo(target)` — each with its own grammar sentence (`Hover over button "Export"`, `Drag file input "Model" onto button "Upload zone"`).
61
+
62
+ Narrated assertions on every control: `expectVisible`, `expectHidden`, `expectEnabled`, `expectDisabled`, `expectFocused`, `expectText`, `expectContainsText`, `expectAttribute`, `expectCount` (for repeated testids) — plus control-specific ones (`Checkbox.expectChecked`, `RadioButton.expectSelected`, `Slider.expectValue`, `FileInput.expectFile`, `Link.expectHref`, `Input.expectEmpty`, `Switch.expectOn/expectOff`, `Table.expectRowCount/expectRowContains`, `Dialog.expectOpen/expectClosed`, `Tabs.expectActive`, `Image.expectAlt/expectLoaded`, `DatePicker.expectDate`, `Combobox.expectSelected`). On failure the event includes `expected` and the `actual` value read from the DOM.
63
+
64
+ ## Locating controls
65
+
66
+ A plain string means `data-testid` — the recommended, stable path (pair it with the auto-testid Vite plugin below). But razo also accepts Playwright's user-facing locators, so it works on apps that were never instrumented:
67
+
68
+ ```ts
69
+ new Button(page, 'export', 'Export'); // data-testid
70
+ new Button(page, { role: 'button', name: 'Export', exact: true }, 'Export');
71
+ new Input(page, { label: 'Filename' }, 'Filename');
72
+ new Input(page, { placeholder: 'my-app e2e' }, 'Project name');
73
+ new Button(page, { text: 'Upload to cloud' }, 'Upload to cloud');
74
+ new Image(page, { altText: 'Model preview' }, 'Model preview');
75
+ new Label(page, { css: '#status' }, 'Export status');
76
+ new Button(page, { locator: page.getByTestId('export').first() }, 'Export'); // escape hatch
77
+ ```
78
+
79
+ The narration never depends on the strategy — only the `StepEvent.selector` changes, and it stays readable (`role=button[name="Export"]`, `label="Filename"`, `css=#status`). Note: a raw `{ locator }` is already bound to the page, so it ignores `{ within }` scoping.
80
+
81
+ ## The AI artifact: wiring the reporter
82
+
83
+ ```ts
84
+ // playwright.config.ts
85
+ export default defineConfig({
86
+ reporter: [['html'], ['@razohq/razo/reporter']],
87
+ // recommended: action/assertion timeouts below the test timeout, so a hanging
88
+ // action fails inside its step (emitting its StepEvent) instead of killing
89
+ // the test with no narration
90
+ expect: { timeout: 3_000 },
91
+ use: { actionTimeout: 5_000 },
92
+ });
93
+ ```
94
+
95
+ The reporter writes one `test-results/<test>/razo-steps.json` per test (configurable via `[['@razohq/razo/reporter', { outputDir: '...' }]]`).
96
+
97
+ ### The `StepEvent` contract
98
+
99
+ Each step in `razo-steps.json` is a `StepEvent` — the artifact contract AI tooling can build on:
100
+
101
+ | Field | Meaning |
102
+ |---|---|
103
+ | `action` | Grammar verb: `click`, `fill`, `choose`, `attach`, `set`, `assert-text`, ... |
104
+ | `controlType` | `button`, `field`, `select`, `slider`, `radio group`, ... |
105
+ | `name` | The control's human name (`"Export"`, `"Filename"`) |
106
+ | `sentence` | The sentence exactly as shown in the Playwright report |
107
+ | `detail` | Action payload (typed text, chosen option, attached file) |
108
+ | `expected` | Assertions: what was expected |
109
+ | `actual` | Failed assertions: what was actually found in the DOM |
110
+ | `selector` | Stable selector (`[data-testid="..."]`) |
111
+ | `status` | `passed` \| `failed` |
112
+ | `error` | Error message (ANSI-free) when failed |
113
+ | `timestamp` | ISO 8601 |
114
+
115
+ The sentence grammar lives in one place — fixed templates per verb — so the narration is predictable and parseable.
116
+
117
+ ## Auto `data-testid` (Vite plugin)
118
+
119
+ The other half: a stable hook with zero maintenance. A component declares who it is and the plugin generates the testid in dev/test:
120
+
121
+ ```html
122
+ <button data-component="SaveButton">Save</button>
123
+ <!-- becomes -->
124
+ <button data-component="SaveButton" data-testid="save-button">Save</button>
125
+ ```
126
+
127
+ **Alignment rule:** `data-testid` = kebab-case of the component name (`NewsletterOptIn` → `newsletter-opt-in`). A manual `data-testid` always wins.
128
+
129
+ ```ts
130
+ // vite.config.ts — register only in dev/test if you don't want it in production
131
+ import { autoTestId, toTestId } from '@razohq/razo/vite';
132
+ export default defineConfig({ plugins: [autoTestId()] });
133
+
134
+ // consuming it from a test
135
+ new Button(page, toTestId('SaveButton'), 'Save'); // getByTestId('save-button')
136
+ ```
137
+
138
+ `vite` is an optional peer dependency — if you don't use the plugin, you don't need it.
139
+
140
+ ## Self-healing locators
141
+
142
+ Deterministic by design: no AI runs inside your tests. Because every control
143
+ already declares a type and a human name, razo can recover from locator drift
144
+ without guessing:
145
+
146
+ - When a control's primary locator stops resolving, razo tries the control's
147
+ explicit `fallbacks` (in order), then the implicit `role + accessible name`
148
+ derived from its type and name. The first spec matching exactly one element
149
+ wins.
150
+
151
+ ```ts
152
+ new Button(page, 'export-v1', 'Export', {
153
+ fallbacks: [{ text: 'Export' }], // optional; tried before role=button[name="Export"]
154
+ });
155
+ ```
156
+
157
+ - A healed step passes and its StepEvent carries
158
+ `healed: { from, to }` — CI stays green, the drift stays visible in the
159
+ report and the artifact.
160
+ - `RAZO_HEALING=fail` turns healing into a diagnosis: the step fails with a
161
+ "locator drift" error naming the working locator. `RAZO_HEALING=off`
162
+ disables it.
163
+ - Healing only triggers when the element cannot be found at all. An element
164
+ that exists but fails an action or assertion fails normally — healing never
165
+ masks a product regression.
166
+ - When nothing heals, the failed StepEvent includes `domCandidates` (same-role
167
+ elements on the page) so `razo-analyze` can propose the corrected locator.
168
+
169
+ ## Notes
170
+
171
+ - razo runs inside Playwright's Node workers only (not component-testing browser context).
172
+ - Loading the CJS entry in your config and the ESM entry in specs is safe: events travel as Playwright attachments, so there is no shared module state.
173
+
174
+ ## License
175
+
176
+ MIT
@@ -0,0 +1,20 @@
1
+ // src/reporting/events.ts
2
+ import { test } from "@playwright/test";
3
+ function stripAnsi(text) {
4
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
5
+ }
6
+ var AI_STEP_ATTACHMENT = "ai-step";
7
+ function emitStepEvent(event) {
8
+ test.info().attachments.push({
9
+ name: AI_STEP_ATTACHMENT,
10
+ contentType: "application/json",
11
+ body: Buffer.from(JSON.stringify(event))
12
+ });
13
+ }
14
+
15
+ export {
16
+ stripAnsi,
17
+ AI_STEP_ATTACHMENT,
18
+ emitStepEvent
19
+ };
20
+ //# sourceMappingURL=chunk-UCLP2ILU.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reporting/events.ts"],"sourcesContent":["import { test } from '@playwright/test';\n\n/**\n * Structured event that accompanies every narrated sentence.\n * This is the artifact an AI analyzer consumes: one entry per action\n * or assertion, with status and business context.\n */\nexport interface StepEvent {\n /** Grammar verb: 'click', 'fill', 'assert-text', ... */\n action: string;\n /** Control type: 'button', 'input', 'select', ... */\n controlType: string;\n /** Human-readable control name (\"Export\", \"Filename\") */\n name: string;\n /** The sentence exactly as it appears in the Playwright report */\n sentence: string;\n /** Action payload (typed text, chosen option) */\n detail?: string;\n /** Assertions only: what was expected */\n expected?: string;\n /** Failed assertions only: what was actually found */\n actual?: string;\n /** Stable selector of the control */\n selector: string;\n status: 'passed' | 'failed';\n /** Error message when status === 'failed' */\n error?: string;\n /** Set when the primary locator stopped resolving and a fallback found the element. */\n healed?: { from: string; to: string };\n /** Failed locator-not-found steps: same-role elements on the page, for the analyzer. */\n domCandidates?: string[];\n timestamp: string;\n}\n\n/** Artifacts are meant for an AI, not a terminal: no ANSI codes. */\nexport function stripAnsi(text: string): string {\n // eslint-disable-next-line no-control-regex\n return text.replace(/\\x1b\\[[0-9;]*m/g, '');\n}\n\n/** Attachment name under which events travel to the reporter. */\nexport const AI_STEP_ATTACHMENT = 'ai-step';\n\n/**\n * Emits a StepEvent as an attachment of the running test. Attachments\n * cross the worker → reporter boundary, so AiReporter can collect them\n * in onTestEnd without shared state.\n */\nexport function emitStepEvent(event: StepEvent): void {\n test.info().attachments.push({\n name: AI_STEP_ATTACHMENT,\n contentType: 'application/json',\n body: Buffer.from(JSON.stringify(event)),\n });\n}\n"],"mappings":";AAAA,SAAS,YAAY;AAmCd,SAAS,UAAU,MAAsB;AAE9C,SAAO,KAAK,QAAQ,mBAAmB,EAAE;AAC3C;AAGO,IAAM,qBAAqB;AAO3B,SAAS,cAAc,OAAwB;AACpD,OAAK,KAAK,EAAE,YAAY,KAAK;AAAA,IAC3B,MAAM;AAAA,IACN,aAAa;AAAA,IACb,MAAM,OAAO,KAAK,KAAK,UAAU,KAAK,CAAC;AAAA,EACzC,CAAC;AACH;","names":[]}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Structured event that accompanies every narrated sentence.
3
+ * This is the artifact an AI analyzer consumes: one entry per action
4
+ * or assertion, with status and business context.
5
+ */
6
+ interface StepEvent {
7
+ /** Grammar verb: 'click', 'fill', 'assert-text', ... */
8
+ action: string;
9
+ /** Control type: 'button', 'input', 'select', ... */
10
+ controlType: string;
11
+ /** Human-readable control name ("Export", "Filename") */
12
+ name: string;
13
+ /** The sentence exactly as it appears in the Playwright report */
14
+ sentence: string;
15
+ /** Action payload (typed text, chosen option) */
16
+ detail?: string;
17
+ /** Assertions only: what was expected */
18
+ expected?: string;
19
+ /** Failed assertions only: what was actually found */
20
+ actual?: string;
21
+ /** Stable selector of the control */
22
+ selector: string;
23
+ status: 'passed' | 'failed';
24
+ /** Error message when status === 'failed' */
25
+ error?: string;
26
+ /** Set when the primary locator stopped resolving and a fallback found the element. */
27
+ healed?: {
28
+ from: string;
29
+ to: string;
30
+ };
31
+ /** Failed locator-not-found steps: same-role elements on the page, for the analyzer. */
32
+ domCandidates?: string[];
33
+ timestamp: string;
34
+ }
35
+ /** Attachment name under which events travel to the reporter. */
36
+ declare const AI_STEP_ATTACHMENT = "ai-step";
37
+ /**
38
+ * Emits a StepEvent as an attachment of the running test. Attachments
39
+ * cross the worker → reporter boundary, so AiReporter can collect them
40
+ * in onTestEnd without shared state.
41
+ */
42
+ declare function emitStepEvent(event: StepEvent): void;
43
+
44
+ export { AI_STEP_ATTACHMENT as A, type StepEvent as S, emitStepEvent as e };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Structured event that accompanies every narrated sentence.
3
+ * This is the artifact an AI analyzer consumes: one entry per action
4
+ * or assertion, with status and business context.
5
+ */
6
+ interface StepEvent {
7
+ /** Grammar verb: 'click', 'fill', 'assert-text', ... */
8
+ action: string;
9
+ /** Control type: 'button', 'input', 'select', ... */
10
+ controlType: string;
11
+ /** Human-readable control name ("Export", "Filename") */
12
+ name: string;
13
+ /** The sentence exactly as it appears in the Playwright report */
14
+ sentence: string;
15
+ /** Action payload (typed text, chosen option) */
16
+ detail?: string;
17
+ /** Assertions only: what was expected */
18
+ expected?: string;
19
+ /** Failed assertions only: what was actually found */
20
+ actual?: string;
21
+ /** Stable selector of the control */
22
+ selector: string;
23
+ status: 'passed' | 'failed';
24
+ /** Error message when status === 'failed' */
25
+ error?: string;
26
+ /** Set when the primary locator stopped resolving and a fallback found the element. */
27
+ healed?: {
28
+ from: string;
29
+ to: string;
30
+ };
31
+ /** Failed locator-not-found steps: same-role elements on the page, for the analyzer. */
32
+ domCandidates?: string[];
33
+ timestamp: string;
34
+ }
35
+ /** Attachment name under which events travel to the reporter. */
36
+ declare const AI_STEP_ATTACHMENT = "ai-step";
37
+ /**
38
+ * Emits a StepEvent as an attachment of the running test. Attachments
39
+ * cross the worker → reporter boundary, so AiReporter can collect them
40
+ * in onTestEnd without shared state.
41
+ */
42
+ declare function emitStepEvent(event: StepEvent): void;
43
+
44
+ export { AI_STEP_ATTACHMENT as A, type StepEvent as S, emitStepEvent as e };
@@ -0,0 +1,336 @@
1
+ import { Page, Locator } from '@playwright/test';
2
+ export { A as AI_STEP_ATTACHMENT, S as StepEvent, e as emitStepEvent } from './events-D_lCLx1V.mjs';
3
+
4
+ interface SentenceContext {
5
+ controlType: string;
6
+ name: string;
7
+ detail?: string;
8
+ expected?: string;
9
+ }
10
+ /**
11
+ * Sentence grammar: one fixed template per verb, in a single place,
12
+ * so the narration is predictable and parseable by an AI.
13
+ */
14
+ declare const SENTENCES: {
15
+ readonly click: (c: SentenceContext) => string;
16
+ readonly fill: (c: SentenceContext) => string;
17
+ readonly clear: (c: SentenceContext) => string;
18
+ readonly select: (c: SentenceContext) => string;
19
+ readonly check: (c: SentenceContext) => string;
20
+ readonly uncheck: (c: SentenceContext) => string;
21
+ readonly choose: (c: SentenceContext) => string;
22
+ readonly 'choose-many': (c: SentenceContext) => string;
23
+ readonly open: (c: SentenceContext) => string;
24
+ readonly attach: (c: SentenceContext) => string;
25
+ readonly set: (c: SentenceContext) => string;
26
+ readonly 'assert-visible': (c: SentenceContext) => string;
27
+ readonly 'assert-enabled': (c: SentenceContext) => string;
28
+ readonly 'assert-disabled': (c: SentenceContext) => string;
29
+ readonly 'assert-text': (c: SentenceContext) => string;
30
+ readonly 'assert-checked': (c: SentenceContext) => string;
31
+ readonly 'assert-unchecked': (c: SentenceContext) => string;
32
+ readonly 'assert-selected': (c: SentenceContext) => string;
33
+ readonly 'assert-value': (c: SentenceContext) => string;
34
+ readonly 'assert-href': (c: SentenceContext) => string;
35
+ readonly hover: (c: SentenceContext) => string;
36
+ readonly 'double-click': (c: SentenceContext) => string;
37
+ readonly 'right-click': (c: SentenceContext) => string;
38
+ readonly press: (c: SentenceContext) => string;
39
+ readonly focus: (c: SentenceContext) => string;
40
+ readonly 'scroll-to': (c: SentenceContext) => string;
41
+ readonly drag: (c: SentenceContext) => string;
42
+ readonly 'assert-hidden': (c: SentenceContext) => string;
43
+ readonly 'assert-focused': (c: SentenceContext) => string;
44
+ readonly 'assert-contains-text': (c: SentenceContext) => string;
45
+ readonly 'assert-attribute': (c: SentenceContext) => string;
46
+ readonly 'assert-empty': (c: SentenceContext) => string;
47
+ readonly 'turn-on': (c: SentenceContext) => string;
48
+ readonly 'turn-off': (c: SentenceContext) => string;
49
+ readonly 'assert-on': (c: SentenceContext) => string;
50
+ readonly 'assert-off': (c: SentenceContext) => string;
51
+ readonly pick: (c: SentenceContext) => string;
52
+ readonly 'assert-row-count': (c: SentenceContext) => string;
53
+ readonly 'assert-row-contains': (c: SentenceContext) => string;
54
+ readonly close: (c: SentenceContext) => string;
55
+ readonly 'assert-open': (c: SentenceContext) => string;
56
+ readonly 'assert-closed': (c: SentenceContext) => string;
57
+ readonly 'open-tab': (c: SentenceContext) => string;
58
+ readonly 'assert-active-tab': (c: SentenceContext) => string;
59
+ readonly 'assert-alt': (c: SentenceContext) => string;
60
+ readonly 'assert-loaded': (c: SentenceContext) => string;
61
+ readonly 'pick-date': (c: SentenceContext) => string;
62
+ readonly 'assert-count': (c: SentenceContext) => string;
63
+ readonly 'assert-tooltip': (c: SentenceContext) => string;
64
+ };
65
+ type StepAction = keyof typeof SENTENCES;
66
+ interface StepOptions {
67
+ /** Action payload (typed text, chosen option) */
68
+ detail?: string;
69
+ /** Assertions only: expected value */
70
+ expected?: string;
71
+ /** On assertion failure: how to read the value actually found */
72
+ readActual?: () => Promise<string>;
73
+ /** Sentence override for business-level narration ("Confirm the export") */
74
+ as?: string;
75
+ }
76
+ interface ControlOptions {
77
+ /** Scope the control inside another one (e.g. a button within a dialog). */
78
+ within?: Control;
79
+ /**
80
+ * Deterministic healing alternatives, tried in order when the primary
81
+ * locator stops resolving. After these, the implicit `role + name`
82
+ * fallback derived from the control type is tried. See RAZO_HEALING.
83
+ */
84
+ fallbacks?: LocatorSpec[];
85
+ }
86
+ type AriaRole = Parameters<Page['getByRole']>[0];
87
+ /**
88
+ * How to find a control. A plain string means data-testid (the recommended,
89
+ * stable path — see the auto-testid Vite plugin); the object forms map 1:1
90
+ * to Playwright's user-facing locators, so razo works on apps that were
91
+ * never instrumented. `{ locator }` is the full escape hatch.
92
+ */
93
+ type LocatorSpec = string | {
94
+ testId: string;
95
+ } | {
96
+ role: AriaRole;
97
+ name?: string;
98
+ exact?: boolean;
99
+ } | {
100
+ label: string;
101
+ exact?: boolean;
102
+ } | {
103
+ placeholder: string;
104
+ } | {
105
+ text: string;
106
+ exact?: boolean;
107
+ } | {
108
+ altText: string;
109
+ } | {
110
+ title: string;
111
+ } | {
112
+ css: string;
113
+ } | {
114
+ locator: Locator;
115
+ };
116
+ declare abstract class Control {
117
+ readonly page: Page;
118
+ readonly name: string;
119
+ protected abstract readonly controlType: string;
120
+ /** @deprecated Only set when the control was located by testid; use `selector`. */
121
+ readonly testId: string;
122
+ private _locator;
123
+ private _selector;
124
+ private readonly parent?;
125
+ private readonly fallbacks;
126
+ /**
127
+ * Root that fallbacks resolve against — read lazily so a child control
128
+ * follows its `within` parent even after the PARENT healed.
129
+ */
130
+ private get healRoot();
131
+ private get withinPrefix();
132
+ /** The Playwright locator currently backing this control. */
133
+ get locator(): Locator;
134
+ /** Readable selector, chained through `within` parents; travels in every StepEvent. */
135
+ get selector(): string;
136
+ constructor(page: Page, locate: LocatorSpec, name: string, options?: ControlOptions);
137
+ /**
138
+ * Core of the framework: runs `fn` inside a test.step titled with the
139
+ * grammar sentence, and emits the matching structured StepEvent —
140
+ * also (especially) when it fails.
141
+ */
142
+ protected step<T>(action: StepAction, options: StepOptions, fn: () => Promise<T>): Promise<T>;
143
+ /** True when the primary locator currently matches nothing (safe: never throws). */
144
+ private primaryGone;
145
+ /** First fallback (explicit, then implicit role+name) resolving to exactly one element. */
146
+ private findHealingSpec;
147
+ /**
148
+ * Same-role elements on the page, as evidence for the analyzer to propose
149
+ * a replacement locator. Best-effort: capped, time-boxed, never throws.
150
+ */
151
+ private collectDomCandidates;
152
+ expectVisible(options?: Pick<StepOptions, 'as'>): Promise<void>;
153
+ expectEnabled(options?: Pick<StepOptions, 'as'>): Promise<void>;
154
+ expectDisabled(options?: Pick<StepOptions, 'as'>): Promise<void>;
155
+ expectText(expected: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
156
+ /** The control as it appears in the narration: `button "Export"`. */
157
+ describe(): string;
158
+ hover(options?: Pick<StepOptions, 'as'>): Promise<void>;
159
+ doubleClick(options?: Pick<StepOptions, 'as'>): Promise<void>;
160
+ rightClick(options?: Pick<StepOptions, 'as'>): Promise<void>;
161
+ press(key: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
162
+ focus(options?: Pick<StepOptions, 'as'>): Promise<void>;
163
+ scrollTo(options?: Pick<StepOptions, 'as'>): Promise<void>;
164
+ dragTo(target: Control, options?: Pick<StepOptions, 'as'>): Promise<void>;
165
+ expectHidden(options?: Pick<StepOptions, 'as'>): Promise<void>;
166
+ expectFocused(options?: Pick<StepOptions, 'as'>): Promise<void>;
167
+ expectContainsText(expected: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
168
+ expectAttribute(attribute: string, expected: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
169
+ /** For repeated testids (list items): how many elements the control matches. */
170
+ expectCount(expected: number, options?: Pick<StepOptions, 'as'>): Promise<void>;
171
+ }
172
+
173
+ declare class Button extends Control {
174
+ protected readonly controlType = "button";
175
+ click(options?: Pick<StepOptions, 'as'>): Promise<void>;
176
+ }
177
+
178
+ declare class Input extends Control {
179
+ protected readonly controlType: string;
180
+ /** For a field, "text" means its value, not its DOM content. */
181
+ expectText(expected: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
182
+ fill(value: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
183
+ clear(options?: Pick<StepOptions, 'as'>): Promise<void>;
184
+ expectEmpty(options?: Pick<StepOptions, 'as'>): Promise<void>;
185
+ }
186
+
187
+ /** Radio group: the testId points at the group container. */
188
+ declare class RadioButton extends Control {
189
+ protected readonly controlType = "radio group";
190
+ select(option: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
191
+ expectSelected(option: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
192
+ }
193
+
194
+ declare class Checkbox extends Control {
195
+ protected readonly controlType = "checkbox";
196
+ private readActualState;
197
+ expectChecked(options?: Pick<StepOptions, 'as'>): Promise<void>;
198
+ expectUnchecked(options?: Pick<StepOptions, 'as'>): Promise<void>;
199
+ check(options?: Pick<StepOptions, 'as'>): Promise<void>;
200
+ uncheck(options?: Pick<StepOptions, 'as'>): Promise<void>;
201
+ }
202
+
203
+ declare class Select extends Control {
204
+ protected readonly controlType = "select";
205
+ /** Chooses an option by its visible label. */
206
+ choose(option: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
207
+ /** Chooses several options by label (for <select multiple>). */
208
+ chooseMany(optionLabels: string[], options?: Pick<StepOptions, 'as'>): Promise<void>;
209
+ }
210
+
211
+ declare class Link extends Control {
212
+ protected readonly controlType = "link";
213
+ open(options?: Pick<StepOptions, 'as'>): Promise<void>;
214
+ expectHref(expected: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
215
+ }
216
+
217
+ /**
218
+ * Read-only element (status messages, headings). It has no actions:
219
+ * it exists so outcome verifications are narrated too.
220
+ */
221
+ declare class Label extends Control {
222
+ protected readonly controlType = "label";
223
+ }
224
+
225
+ declare class FileInput extends Control {
226
+ protected readonly controlType = "file input";
227
+ /** Attaches one or more files by path. The narration shows the file names. */
228
+ attach(files: string | string[], options?: Pick<StepOptions, 'as'>): Promise<void>;
229
+ clear(options?: Pick<StepOptions, 'as'>): Promise<void>;
230
+ /** Asserts the name of the file currently selected. */
231
+ expectFile(expectedName: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
232
+ }
233
+
234
+ declare class Slider extends Control {
235
+ protected readonly controlType = "slider";
236
+ /**
237
+ * Sets the slider value. Range inputs cannot be filled, so the value is
238
+ * set in the DOM and the input/change events the app listens to are fired.
239
+ */
240
+ setValue(value: number | string, options?: Pick<StepOptions, 'as'>): Promise<void>;
241
+ expectValue(expected: number | string, options?: Pick<StepOptions, 'as'>): Promise<void>;
242
+ }
243
+
244
+ /** Same behavior as Input; only the narration names it differently. */
245
+ declare class TextArea extends Input {
246
+ protected readonly controlType = "text area";
247
+ }
248
+
249
+ /** ARIA switch (role="switch" with aria-checked). Actions are idempotent. */
250
+ declare class Switch extends Control {
251
+ protected readonly controlType = "switch";
252
+ private readState;
253
+ turnOn(options?: Pick<StepOptions, 'as'>): Promise<void>;
254
+ turnOff(options?: Pick<StepOptions, 'as'>): Promise<void>;
255
+ expectOn(options?: Pick<StepOptions, 'as'>): Promise<void>;
256
+ expectOff(options?: Pick<StepOptions, 'as'>): Promise<void>;
257
+ }
258
+
259
+ /**
260
+ * ARIA combobox: the testId points at the text input; options are looked up
261
+ * by role anywhere on the page (comboboxes often render their listbox in a
262
+ * portal outside the input's subtree).
263
+ */
264
+ declare class Combobox extends Control {
265
+ protected readonly controlType = "combobox";
266
+ search(text: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
267
+ pick(option: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
268
+ expectSelected(expected: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
269
+ }
270
+
271
+ /** Read-only table assertions over tbody rows. */
272
+ declare class Table extends Control {
273
+ protected readonly controlType = "table";
274
+ private rows;
275
+ expectRowCount(expected: number, options?: Pick<StepOptions, 'as'>): Promise<void>;
276
+ expectRowContains(expected: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
277
+ }
278
+
279
+ /** Native <dialog> or role="dialog". close() sends Escape. */
280
+ declare class Dialog extends Control {
281
+ protected readonly controlType = "dialog";
282
+ private readState;
283
+ close(options?: Pick<StepOptions, 'as'>): Promise<void>;
284
+ expectOpen(options?: Pick<StepOptions, 'as'>): Promise<void>;
285
+ expectClosed(options?: Pick<StepOptions, 'as'>): Promise<void>;
286
+ }
287
+
288
+ /** ARIA tablist: the testId points at the tablist container. */
289
+ declare class Tabs extends Control {
290
+ protected readonly controlType = "tabs";
291
+ open(tab: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
292
+ expectActive(expected: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
293
+ }
294
+
295
+ declare class Image extends Control {
296
+ protected readonly controlType = "image";
297
+ expectAlt(expected: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
298
+ expectLoaded(options?: Pick<StepOptions, 'as'>): Promise<void>;
299
+ }
300
+
301
+ /** Native input[type="date"]; dates travel in ISO format (YYYY-MM-DD). */
302
+ declare class DatePicker extends Control {
303
+ protected readonly controlType = "date picker";
304
+ pick(isoDate: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
305
+ expectDate(expected: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
306
+ }
307
+
308
+ /**
309
+ * Dropdown menu: the testId points at the trigger button; menu items are
310
+ * looked up by role anywhere on the page (menus often render in portals).
311
+ */
312
+ declare class Menu extends Control {
313
+ protected readonly controlType = "menu";
314
+ choose(item: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
315
+ }
316
+
317
+ /**
318
+ * Tooltip: the testId points at the TRIGGER element. The tooltip content is
319
+ * resolved through the trigger's aria-describedby (the accessible pattern),
320
+ * so it works wherever the tooltip node is rendered.
321
+ */
322
+ declare class Tooltip extends Control {
323
+ protected readonly controlType = "tooltip";
324
+ private tooltipText;
325
+ /** Hovers the trigger and asserts the tooltip content. */
326
+ expectContent(expected: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
327
+ }
328
+
329
+ /** contenteditable region. Inherits expectText/expectContainsText from the base. */
330
+ declare class Editor extends Control {
331
+ protected readonly controlType = "editor";
332
+ write(text: string, options?: Pick<StepOptions, 'as'>): Promise<void>;
333
+ clear(options?: Pick<StepOptions, 'as'>): Promise<void>;
334
+ }
335
+
336
+ export { Button, Checkbox, Combobox, Control, type ControlOptions, DatePicker, Dialog, Editor, FileInput, Image, Input, Label, Link, type LocatorSpec, Menu, RadioButton, Select, Slider, type StepAction, type StepOptions, Switch, Table, Tabs, TextArea, Tooltip };