UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

177 lines 9.5 kB
"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