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 { 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 = ""; 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((resolve) => setTimeout(resolve, Math.min(pollMs, remaining))); } } }