626d06327c
New domain `browser` under frontend/functions/ with 6 Playwright helpers: - pw_launch_browser: chromium + context + page bootstrap with storageState support and baseUrl navigation. - pw_kanban_login: authenticates a Page against /api/auth/login; sets the kanban_session cookie via shared storageState; verifies login page no longer visible after navigation. - pw_drag_drop: human-like pointer drag (mousedown + activateOffset + stepped move + mouseup) compatible with @dnd-kit/core's 8px activation threshold; supports hoverMs for time-based dropzones. - pw_keyboard_sequence: ordered focus/type/press/wait steps for scripting realistic input flows (typing then arrow-key navigating autocompletes). - pw_wait_predicate: thin wrapper over page.waitForFunction with friendlier defaults and custom error messages. - pw_assert_class: poll-based assertion that a Locator has/lacks a CSS class within a timeout; useful for visual-state checks. Each function ships with vitest tests (5-8 cases each) covering both happy and error paths, plus self-documenting .md (Ejemplo + Cuando usarla + Gotchas + frontmatter with params/output schema). Adds frontend/functions/package.json with `"type": "module"` so consumers can ESM-import the .ts files from anywhere in the registry (Playwright's tsx loader respects nearest package.json). Capability page docs/capabilities/playwright.md documents the group with a canonical end-to-end example, frontiers, prerequisites, and gotchas. Index updated. First consumer (issue 0088): apps/kanban requester-input.spec.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
import type { Locator, Page } from "playwright";
|
|
|
|
/** Options for asserting a CSS class on a Playwright Locator. */
|
|
export interface PwAssertClassOptions {
|
|
/** CSS selector string or an existing Playwright Locator. */
|
|
selector: string | Locator;
|
|
/** CSS class to check, without the leading dot (e.g. "border-red"). */
|
|
className: string;
|
|
/** If true (default), asserts the class IS present. If false, asserts it is NOT present. */
|
|
mustHave?: boolean;
|
|
/** Max milliseconds to poll for the condition. Default: 5000. */
|
|
timeoutMs?: number;
|
|
}
|
|
|
|
/**
|
|
* pw_assert_class — asserts a Playwright Locator has (or lacks) a given CSS class.
|
|
*
|
|
* Polls the element up to timeoutMs. Throws with a descriptive message if the
|
|
* condition is not met in time.
|
|
*
|
|
* When selector is a string, resolves via page.locator(selector).first() and
|
|
* delegates to Playwright's native waitForFunction for efficiency.
|
|
* When selector is a Locator, uses a manual polling loop with locator.evaluate()
|
|
* because Locator objects do not serialize across waitForFunction page boundaries.
|
|
*/
|
|
export async function pw_assert_class(page: Page, opts: PwAssertClassOptions): Promise<void> {
|
|
const { selector, className } = opts;
|
|
const mustHave = opts.mustHave ?? true;
|
|
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
|
|
if (typeof selector === "string") {
|
|
// String selector: use waitForFunction for efficient native polling.
|
|
const selectorRepr = selector;
|
|
try {
|
|
await page.waitForFunction(
|
|
({ sel, cls, must }: { sel: string; cls: string; must: boolean }) => {
|
|
const el = document.querySelector(sel);
|
|
if (!el) return false;
|
|
const has = el.classList.contains(cls);
|
|
return must ? has : !has;
|
|
},
|
|
{ sel: selector, cls: className, must: mustHave },
|
|
{ timeout: timeoutMs }
|
|
);
|
|
} catch {
|
|
throw new Error(
|
|
`expected ${selectorRepr} to ${mustHave ? "have" : "NOT have"} class ${className} within ${timeoutMs}ms`
|
|
);
|
|
}
|
|
} else {
|
|
// Locator: manual polling loop — Locators don't cross waitForFunction boundary.
|
|
const locator = selector;
|
|
const selectorRepr = "<Locator>";
|
|
const deadline = Date.now() + timeoutMs;
|
|
const pollMs = 100;
|
|
|
|
while (true) {
|
|
let conditionMet = false;
|
|
try {
|
|
const has = await locator.evaluate(
|
|
(el: Element, cls: string) => el.classList.contains(cls),
|
|
className
|
|
);
|
|
conditionMet = mustHave ? has : !has;
|
|
} catch {
|
|
// Element may not be attached yet — treat as condition not met.
|
|
conditionMet = false;
|
|
}
|
|
|
|
if (conditionMet) return;
|
|
|
|
const remaining = deadline - Date.now();
|
|
if (remaining <= 0) {
|
|
throw new Error(
|
|
`expected ${selectorRepr} to ${mustHave ? "have" : "NOT have"} class ${className} within ${timeoutMs}ms`
|
|
);
|
|
}
|
|
|
|
await new Promise<void>((resolve) => setTimeout(resolve, Math.min(pollMs, remaining)));
|
|
}
|
|
}
|
|
}
|