donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
177 lines • 9.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlaywrightAssertionStepSchema = void 0;
exports.buildAssertExecutor = buildAssertExecutor;
exports.buildLocateExecutor = buildLocateExecutor;
const test_1 = require("@playwright/test");
const v4_1 = require("zod/v4");
const TemplateInterpolator_1 = require("../../../utils/TemplateInterpolator");
const buildLocator_1 = require("../locate/buildLocator");
// ---------------------------------------------------------------------------
// Structured assertion step schema
// ---------------------------------------------------------------------------
/**
* A single structured assertion step returned by the AI. Each step maps
* deterministically to one Playwright `expect` call — no free-form code
* generation, no VM evaluation.
*/
exports.PlaywrightAssertionStepSchema = v4_1.z.object({
/** How to locate the element. Set to `null` for page-level assertions (title, url). */
locator: v4_1.z
.enum(['role', 'text', 'label'])
.nullable()
.describe(`The locator STRATEGY — always one of the literal strings 'role', 'text', 'label', or null. Never set this to an ARIA role name (e.g. 'link', 'button') — those belong in the separate 'role' field.
- 'role': Use page.getByRole(role, { name: value }) — best for semantic elements (headings, buttons, links, tabs, etc.)
- 'text': Use page.getByText(value) — best for checking text content visibility
- 'label': Use page.getByLabel(value) — best for form fields (text inputs, checkboxes, radio buttons, selects) identified by their label text. Prefer this over 'role'='textbox' when checking input values or checked state.
- null: For page-level assertions (toHaveTitle, toHaveURL) where no element locator is needed`),
/** ARIA role name. Required when locator is 'role', otherwise null. */
role: v4_1.z
.string()
.nullable()
.describe(`The ARIA role name passed to page.getByRole() — only set when locator is 'role', otherwise null. This is the semantic role of the element, NOT the locator strategy.
Common roles: 'heading', 'button', 'link', 'tab', 'tabpanel', 'dialog', 'navigation', 'textbox', 'checkbox', 'radio', 'combobox', 'menuitem'.`),
/** The text, name, title, URL, or regex pattern to match. */
value: v4_1.z.string().describe(`The value to match against. Its meaning depends on the locator and assertion:
- For locator='role': the accessible name passed to { name: value }
- For locator='text': the text content to find via page.getByText()
- For locator='label': the label text passed to page.getByLabel()
- For assertion='toHaveTitle': the expected page title
- For assertion='toHaveURL': the expected page URL or URL pattern
- For assertion='toHaveValue': the expected input field value (used with locator='label')
- For assertion='toHaveAttribute': the attribute name (use attributeValue for the expected value)`),
/** Whether 'value' should be interpreted as a regex pattern. */
valueIsRegex: v4_1.z
.boolean()
.describe('Set to true when value is a regex pattern (e.g. "Page \\d+ of \\d+"). The value will be passed to new RegExp().'),
/** The Playwright assertion to apply. */
assertion: v4_1.z
.enum([
'toBeVisible',
'toBeHidden',
'toBeEnabled',
'toBeDisabled',
'toBeChecked',
'toHaveValue',
'toContainText',
'toHaveAttribute',
'toHaveTitle',
'toHaveURL',
])
.describe(`The Playwright assertion to apply:
- 'toBeVisible': expect(locator).toBeVisible() — element is present and visible
- 'toBeHidden': expect(locator).not.toBeVisible() — element is not visible (or absent)
- 'toBeEnabled': expect(locator).toBeEnabled() — interactive element is enabled
- 'toBeDisabled': expect(locator).toBeDisabled() — interactive element is disabled
- 'toBeChecked': expect(locator).toBeChecked() — checkbox, radio button, or toggle switch is selected/on. Use this for checked state, NOT toBeVisible.
- 'toHaveValue': expect(locator).toHaveValue(attributeValue) — input/textarea/select has the given value. Use with locator='label' and set attributeValue to the expected text. Do NOT use toBeVisible for this.
- 'toContainText': expect(locator).toContainText(attributeValue) — element contains the given text as a substring
- 'toHaveAttribute': expect(locator).toHaveAttribute(value, attributeValue) — element has the given attribute with the given value. value is the attribute name (e.g. 'aria-selected'), attributeValue is the expected value (e.g. 'true').
- 'toHaveTitle': expect(page).toHaveTitle(value) — page title matches (set locator to null)
- 'toHaveURL': expect(page).toHaveURL(value) — page URL matches (set locator to null)`),
/**
* Secondary value used by assertions that require two values.
* For `toHaveValue`: the expected input field value.
* For `toHaveAttribute`: the expected attribute value (value holds the attribute name).
* For `toContainText`: the text substring to match (value holds the locator match text).
* Null for all other assertions.
*/
attributeValue: v4_1.z
.string()
.nullable()
.describe(`A secondary string used by assertions that need two values:
- toHaveValue: set to the expected input value (e.g. "My Device Name")
- toHaveAttribute: set to the expected attribute value (e.g. "true" when value="aria-selected")
- toContainText: set to the text substring to match within the element
- All other assertions: set to null`),
});
/**
* Resolves any `{{$.env.X}}` placeholders in a step field against the
* supplied env data. Returns the input verbatim when no env data is given,
* preserving backwards compatibility with cached entries that contain
* literal values only.
*/
function resolveStepField(value, envData) {
if (!envData || !value.includes('{{')) {
return value;
}
return (0, TemplateInterpolator_1.interpolateString)(value, { env: envData, calls: [] });
}
/**
* Builds an executor function from structured assertion steps.
* Each step maps to exactly one Playwright `expect` call — no string
* evaluation, no VM contexts.
*/
function buildAssertExecutor(steps) {
return async ({ page, envData }) => {
for (const step of steps) {
const resolvedValue = resolveStepField(step.value, envData);
const resolvedAttrValue = step.attributeValue === null
? null
: resolveStepField(step.attributeValue, envData);
const matcher = step.valueIsRegex
? new RegExp(resolvedValue)
: resolvedValue;
// Page-level assertions (no element locator needed)
if (step.assertion === 'toHaveTitle') {
await (0, test_1.expect)(page).toHaveTitle(matcher);
continue;
}
if (step.assertion === 'toHaveURL') {
await (0, test_1.expect)(page).toHaveURL(matcher);
continue;
}
// Element-level assertions
let locator;
if (step.locator === 'role' && step.role) {
locator = page.getByRole(step.role, { name: matcher });
}
else if (step.locator === 'label') {
locator = page.getByLabel(matcher);
}
else {
locator = page.getByText(matcher);
}
// Always narrow to the first match. Cached assertions have already been
// verified against the live page; on replay the same selector may match
// additional elements (e.g. sidebar + main content) which triggers
// Playwright's strict-mode error. Using .first() is safe because the
// assertion only needs at least one matching element to satisfy the condition.
locator = locator.first();
switch (step.assertion) {
case 'toBeVisible':
await (0, test_1.expect)(locator).toBeVisible();
break;
case 'toBeHidden':
await (0, test_1.expect)(locator).not.toBeVisible();
break;
case 'toBeEnabled':
await (0, test_1.expect)(locator).toBeEnabled();
break;
case 'toBeDisabled':
await (0, test_1.expect)(locator).toBeDisabled();
break;
case 'toBeChecked':
await (0, test_1.expect)(locator).toBeChecked();
break;
case 'toHaveValue':
await (0, test_1.expect)(locator).toHaveValue(resolvedAttrValue ?? '');
break;
case 'toContainText':
await (0, test_1.expect)(locator).toContainText(resolvedAttrValue ?? '');
break;
case 'toHaveAttribute':
await (0, test_1.expect)(locator).toHaveAttribute(resolvedValue, resolvedAttrValue ?? '');
break;
}
}
};
}
/**
* Builds a cache executor that mechanically reconstructs a Playwright
* {@link Locator} from a cached {@link LocateResult}.
*/
function buildLocateExecutor(result) {
return ({ page, envData }) => (0, buildLocator_1.buildLocator)(page, result, envData);
}
//# sourceMappingURL=assertCache.js.map