import { Locator } from '@playwright/test'; import { ElementRepository, WebElement } from '@civitas-cerebrum/element-repository'; import { ElementInteractions } from '../interactions/facade/ElementInteractions'; import { DropdownSelectOptions, TextVerifyOptions, CountVerifyOptions, DragAndDropOptions, ScreenshotOptions, IsVisibleOptions } from '../enum/Options'; import { ElementSnapshot, ExpectBuilder, ExpectContext, BooleanMatcher } from './ExpectMatchers'; import { VisibleChain } from './VisibleChain'; /** * The shape returned by the `ElementAction.visible` getter: it is BOTH the * matcher-tree boolean field (`.visible.toBeTrue()`, `.visible.not.toBe(false)`) * AND callable as the visible-selection strategy (`.visible().click()`). The * matcher form is property access; the strategy form is a call. This dual shape * exists because `.visible` was already the established matcher-tree getter when * the visible-selection strategy was added — overloading the single name keeps * both call sites working without a breaking rename. * * The members are spelled out (rather than `BooleanMatcher & callable`) so the * type matches the runtime exactly: `timeout` and `not` step OFF the callable * and onto a plain `BooleanMatcher` — mirroring the implementation, which * returns the underlying matcher there. Modelling them as a callable * `VisibleField` would let `.visible.timeout(500)()` typecheck and then throw at * runtime (the matcher is not callable). */ export type VisibleField = (() => ElementAction) & { /** Assert the resolved boolean equals `expected`. */ toBe(expected: boolean): ExpectBuilder; /** Assert the element is visible. */ toBeTrue(): ExpectBuilder; /** Assert the element is not visible. */ toBeFalse(): ExpectBuilder; /** Apply a per-assertion timeout; resolves to a plain `BooleanMatcher` (no longer the strategy callable). */ timeout(ms: number): BooleanMatcher; /** Negate the assertion; a plain `BooleanMatcher` (no longer the strategy callable). */ readonly not: BooleanMatcher; }; /** * Fluent builder for performing actions on a repository element. * * Usage: * ```ts * await steps.on('submitButton', 'LoginPage').click(); * await steps.on('navItems', 'HomePage').random().hover(); * await steps.on('productCards', 'CollectionsPage').nth(2).getText(); * ``` */ export declare class ElementAction { private _repo; private _elementName; private _pageName; private interactions; private timeoutMs?; private resolutionOptions; private _timeout; private conditionalVisible; private visibilityTimeout; private visibleStrategy; /** * When set, this chain queries WITHIN a parent element instead of resolving * `elementName`/`pageName` against the repository. The factory takes the * resolved parent `Locator` and returns the un-narrowed child `Locator` * (e.g. `parent.getByRole(...)`). Seeded by `findByRole` / `findByText` / * `findBySelector`; `resolve()` / `resolveAll()` apply the chain's strategy * selectors to the returned locator so every existing terminal composes. */ private scopedChild?; constructor(_repo: ElementRepository, _elementName: string, _pageName: string, interactions: ElementInteractions, timeoutMs?: number | undefined); /** Repository this chain resolves elements against. Readonly — set at construction. */ get repo(): ElementRepository; /** Element name on the target page. Readonly — set at construction. */ get elementName(): string; /** Page name in the repository. Readonly — set at construction. */ get pageName(): string; /** * Override the retry timeout for any subsequent matcher or predicate call * on this chain. Mutates self and returns `this` for fluent chaining — * consistent with strategy selectors like `.first()` and `.nth()`. * * @example * await steps.on('slowWidget', 'Page').timeout(5000).text.toBe('Ready'); * await steps.on('btn', 'Page').nth(2).timeout(1000).visible.toBeTrue(); */ timeout(ms: number): this; /** Select the first matching element (default behavior). */ first(): this; /** * Strategy selector: among duplicate matches, select the *visible* one, then * compose with any terminal action / verification — exactly like `.first()`, * but skipping hidden matches. Resolves via the repository's `getVisible(...)` * with `strict: true`, so it throws when no visible match exists — consistent * with every other strategy selector. * * Distinct from {@link ifVisible} / {@link isVisible}, which *conditionally * skip* when the element is hidden — this *selects* the visible duplicate and * proceeds. Use it to disambiguate responsive duplicate elements (e.g. a * desktop/mobile pair where only one is rendered at the current viewport). * * Reached as `steps.on(el, page).visible()`. Note `steps.on(el, page).visible` * (no call) is the matcher-tree boolean field — `.visible.toBeTrue()`; calling * it (`.visible()`) switches the chain into visible-selection mode. * * @example * ```ts * await steps.on('navMenu', 'HomePage').visible().click(); * await steps.on('cta', 'HomePage').visible().verifyState('visible'); * ``` */ private selectVisible; /** Select a random matching element. */ random(): this; /** Select the element at the given zero-based index. */ nth(index: number): this; /** Select the first element matching the given text content. */ byText(text: string): this; /** Select the first element matching the given attribute name-value pair. */ byAttribute(name: string, value: string): this; /** * Makes all subsequent actions conditional on visibility. * If the element is not visible within the timeout, actions silently skip * instead of throwing. Returns `this` for chaining. * * @param timeout - Max wait in ms to check visibility. Defaults to `2000`. * * @example * ```ts * await steps.on('cookieBanner', 'Page').ifVisible().click(); * await steps.on('promoPopup', 'Page').ifVisible(500).click(); * ``` * * @deprecated Prefer `await steps.on(el, page).isVisible({ timeout }).click()`. * `isVisible()` is the unified replacement for both `ifVisible()` (modifier) * and the old boolean `isVisible()` probe. Will be removed in a future major release. */ ifVisible(timeout?: number): this; /** * Checks the ifVisible condition. Returns `true` if the action should proceed, * `false` if it should be skipped. */ private shouldProceed; private resolve; private resolveAll; /** * Raw, un-waited target for visibility probes (`VisibleChain`). Scoped * chains resolve through `scopedChild` — the repository has no entry for * the stamped scoped name; repository chains construct the `WebElement` * straight from the raw selector so the probe's own `timeout` is the only * wait applied (avoiding the repo-resolution default `repo.get` imposes). * Note the scoped path resolves the PARENT via the repository first, so a * missing parent pays that resolution wait before the probe reports false. */ probeTarget(): Promise; /** * Apply this chain's strategy selectors to a scoped child locator. `.nth(i)` * narrows by index, the collection strategy returns the whole set, * `.visible()` filters to the visible match, and the default / `.first()` * resolves the first match. RANDOM / TEXT / ATTRIBUTE are rejected: a * scoped `findBy*` query already carries its own role/text/selector filter, * so layering a second repo-style strategy on top is ambiguous — we fail * fast rather than silently behave like `.first()`. * Use `.nth(i)` (or a more specific `findBy*` query) to disambiguate. */ private narrowScoped; /** * Spawn a fresh `ElementAction` that queries WITHIN this element. The new * chain reuses the same repo / interactions / timeout but resolves through * the given child-locator factory instead of the repository, so it composes * with every existing terminal (`.count`, `.verifyState`, `.click`, * `.getText`, `.first()` / `.nth()`, the matcher tree, …). */ private spawnScoped; /** * Query by ARIA role WITHIN this element. Scopes `parent.getByRole(role, options)`. * @example * await steps.on('cookieDialog', 'CookieBanner').findByRole('button').count.toBe(2); * await steps.on('table', 'TablePage').findByRole('cell', { name: 'Alice Martin' }).getText(); */ findByRole(role: Parameters[0], options?: { name?: string | RegExp; exact?: boolean; }): ElementAction; /** * Query by text content WITHIN this element. Scopes `parent.getByText(text, options)`. * @example * await steps.on('cartPanel', 'CartPage').findByText('Your cart is empty').verifyState('visible'); */ findByText(text: string | RegExp, options?: { exact?: boolean; }): ElementAction; /** * Query by raw CSS selector WITHIN this element. Scopes `parent.locator(css)`. * @example * await steps.on('panel', 'Page').findBySelector("input[name='email']").fill('a@b.com'); */ findBySelector(css: string): ElementAction; /** Click the resolved element. Skips silently if `ifVisible()` was set and element is hidden. */ click(options?: { withoutScrolling?: boolean; force?: boolean; }): Promise; /** Click the resolved element if present. Returns `true` if clicked, `false` if skipped. */ clickIfPresent(options?: { withoutScrolling?: boolean; force?: boolean; }): Promise; /** Hover over the resolved element. Skips silently if `ifVisible()` was set and element is hidden. */ hover(): Promise; /** Clear and fill the resolved element with text. Skips silently if `ifVisible()` was set and element is hidden. */ fill(text: string): Promise; /** Scroll the resolved element into view. Skips silently if `ifVisible()` was set and element is hidden. */ scrollIntoView(): Promise; /** Select a dropdown option. */ selectDropdown(options?: DropdownSelectOptions): Promise; /** Check a checkbox or radio button. Skips silently if `ifVisible()` was set and element is hidden. */ check(): Promise; /** Uncheck a checkbox. Skips silently if `ifVisible()` was set and element is hidden. */ uncheck(): Promise; /** Double-click the resolved element. */ doubleClick(): Promise; /** Right-click the resolved element. */ rightClick(): Promise; /** Type text character by character. */ typeSequentially(text: string, delay?: number): Promise; /** Upload one or more files to a file input. Pass a string array for multi-file inputs. */ uploadFile(filePath: string | string[]): Promise; /** Simulate dropping files onto a drop-zone element via DataTransfer drag events. */ dropFiles(filenames: string[], mimeType?: string): Promise; /** Drag and drop the resolved element. */ dragAndDrop(options: DragAndDropOptions): Promise; /** Clear the input value. */ clearInput(): Promise; /** Set slider value. */ setSliderValue(value: number): Promise; /** Select multiple options from a multi-select. */ selectMultiple(values: string[]): Promise; /** Assert the element is visible. Delegates to the matcher tree's `.visible.toBeTrue()`. */ verifyPresence(): Promise; /** * Assert the element is hidden or detached. Uses Playwright's * `expect(locator).toBeHidden()` on the raw selector — never calls * `repo.get(...)` because that would pay the 15s repo-resolution wait * waiting for the element to become attached, which is the opposite of * what we want when asserting absence. * * Scoped `findBy*` chains assert on the child locator itself (the stamped * scoped name has no repository entry). Resolving the PARENT still waits * for it: asserting "child absent" requires the parent to exist. */ verifyAbsence(): Promise; /** * Assert the element's text content. Call with no argument to assert "not empty". * * @param expected - Expected exact text. Omit to assert the element has any non-empty text. * @param options - Optional verification options. Passing `{ notEmpty: true }` * is redundant — omit `expected` to get the same behavior. The `notEmpty` * flag on `TextVerifyOptions` is itself deprecated. */ verifyText(expected?: string, options?: TextVerifyOptions): Promise; /** Assert text contains a substring. Delegates to the matcher tree's `.text.toContain(...)`. */ verifyTextContains(expected: string): Promise; /** Assert the element count. Delegates to the matcher tree's count matchers. */ verifyCount(options: CountVerifyOptions): Promise; /** Check if element is visible (boolean, no assertion). */ isPresent(): Promise; /** * Unified visibility entry point. Returns a `VisibleChain` that is both: * * - **awaitable as `Promise`** — the probe, never throws. Backwards * compatible with the old `isVisible(): Promise` signature — * `await steps.on(el, page).isVisible({ timeout: 500 })` still resolves * to a boolean at runtime. * - **chainable with action methods and the matcher tree** — the gate, * silently skips when the element is hidden. Replaces `ifVisible()`. * * Every probe and gate decision is logged under `tester:visible` with a * `[probe]` or `[gate]` tag so silently-skipped actions stay debuggable. * * @param options - `{ timeout?: 2000, containsText?: string }`. When * `containsText` is provided, the probe is `true` only if the element is * visible AND its text contains the given substring. Note: matcher-tree * gates (`.isVisible().text.toBe(...)`) only honor the visibility check — * `containsText` applies to probe + action-gate paths. * * @example * ```ts * // Probe * if (await steps.on('banner', 'Page').isVisible({ timeout: 500 })) { … } * * // Gate * await steps.on('cookieBanner', 'Page').isVisible().click(); * await steps.on('promo', 'Page').isVisible({ timeout: 500 }).text.toBe('Promo'); * ``` */ isVisible(options?: IsVisibleOptions): VisibleChain; /** Assert an attribute value. Delegates to the matcher tree's `.attributes.get(name).toBe(value)`. */ verifyAttribute(attributeName: string, expectedValue: string): Promise; /** Assert input value. Delegates to the matcher tree's `.value.toBe(expectedValue)`. */ verifyInputValue(expectedValue: string): Promise; /** * Assert every matched image has a real `src`, non-zero `naturalWidth`, and * decodes successfully. Collection-level — resolves with * `SelectionStrategy.ALL` regardless of any strategy selector. */ verifyImages(scroll?: boolean, options?: { verifyDecoded?: boolean; }): Promise; /** Assert element state. */ verifyState(state: 'enabled' | 'disabled' | 'editable' | 'checked' | 'focused' | 'visible' | 'hidden' | 'attached' | 'inViewport'): Promise; /** Assert CSS property value. Delegates to the matcher tree's `.css(property).toBe(value)`. */ verifyCssProperty(property: string, expectedValue: string): Promise; /** * Assert all matched elements appear in the exact text order specified. * * Collection-level — ignores any `.first()` / `.nth()` / `.random()` strategy * on the chain and resolves with `SelectionStrategy.ALL` so the full list is * compared against `expectedTexts`. */ verifyOrder(expectedTexts: string[]): Promise; /** * Assert all matched elements are sorted in the given direction. * * Collection-level — resolves with `SelectionStrategy.ALL` regardless of any * strategy selector on the chain. */ verifyListOrder(direction: 'asc' | 'desc'): Promise; /** Get the text content of the resolved element. */ getText(): Promise; /** Get an attribute value. */ getAttribute(name: string): Promise; /** Get the count of matching elements. */ getCount(): Promise; /** Get all text contents from matching elements. */ getAllTexts(): Promise; /** Get input value. */ getInputValue(): Promise; /** Get computed CSS property value. */ getCssProperty(property: string): Promise; /** * Get the raw HTML of the resolved element. Defaults to `innerHTML`; * pass `{ outer: true }` to get `outerHTML` (the element tag + subtree). */ getHtml(options?: { outer?: boolean; }): Promise; /** Assert the element's HTML equals the expected string exactly. Delegates to the matcher tree's `.html.toBe(...)` (or `.outerHtml.toBe(...)` when `outer`). */ verifyHtml(expected: string, options?: { outer?: boolean; }): Promise; /** Assert the element's HTML contains a substring. Delegates to the matcher tree's `.html.toContain(...)`. */ verifyHtmlContains(substring: string, options?: { outer?: boolean; }): Promise; /** Assert the element's HTML matches a regex. Delegates to the matcher tree's `.html.toMatch(...)`. */ verifyHtmlMatches(regex: RegExp, options?: { outer?: boolean; }): Promise; /** Take a screenshot of the element. */ screenshot(options?: ScreenshotOptions): Promise; /** * Captures a snapshot of the element's state at the current moment. Used * by the matcher tree (`.text.toBe(...)`, `.count.toBeGreaterThan(...)`, * etc.) and by the predicate form of `expect(...)`. * * Snapshot fields are all primitives — no async access needed in predicates. */ captureSnapshot(): Promise; /** Build the context object consumed by the matcher tree classes. */ buildExpectContext(): ExpectContext; /** * Matcher tree rooted at this element. All field matchers (`text`, `value`, * `count`, `visible`, `enabled`, `attributes`, `css(...)`) and the * predicate form (`satisfy(pred)`) are exposed via an internal `ExpectBuilder` * so the surface stays consistent between `steps.on()` and `steps.expect()`. */ private expectBuilder; get text(): import("./ExpectMatchers").TextMatcher; get value(): import("./ExpectMatchers").ValueMatcher; get count(): import("./ExpectMatchers").CountMatcher; /** * Dual-purpose: the matcher-tree boolean field AND the visible-selection * strategy. As property access (`.visible.toBeTrue()`) it is the * `BooleanMatcher`. Called (`.visible().click()`) it switches the chain into * visible-selection mode (see {@link selectVisible}) and returns `this` so * terminal actions/verifications compose like `.first()`. */ get visible(): VisibleField; get enabled(): BooleanMatcher; get attributes(): import("./ExpectMatchers").AttributesMatcher; get html(): import("./ExpectMatchers").HtmlMatcher; get outerHtml(): import("./ExpectMatchers").HtmlMatcher; css(property: string): import("./ExpectMatchers").CssMatcher; /** * Returns a negated matcher tree. Flip the expected outcome of any matcher * reached from this object. * * @example * await steps.on('error', 'Page').not.text.toContain('Error'); * await steps.on('submitBtn', 'Page').not.enabled.toBe(false); */ get not(): ExpectBuilder; /** * Predicate escape hatch. Queues a custom predicate assertion and returns * the chain builder so more matchers can follow. End the chain with * `.throws(message)` to override the failure message. * * Named `satisfy` to avoid overlap with field-matcher `.text.toBe('x')` * which asserts value equality on a specific field. * * @example * await steps.on('price', 'ProductPage') * .satisfy(el => parseFloat(el.text.slice(1)) > 10) * .throws('price must be above $10'); */ satisfy(predicate: (el: ElementSnapshot) => boolean): ExpectBuilder; /** Wait for the element to reach the specified state. */ waitForState(state?: 'visible' | 'attached' | 'hidden' | 'detached'): Promise; }