import { describe, it, expect, vi, beforeEach } from "vitest"; import type { Page, Locator } from "playwright"; import { pw_assert_class, type PwAssertClassOptions } from "./pw_assert_class"; // --------------------------------------------------------------------------- // Mock helpers // --------------------------------------------------------------------------- /** Build a mock Page with configurable waitForFunction behavior. */ function makeMockPage(opts: { waitForFunctionResolves?: boolean; waitForFunctionError?: Error; } = {}): Page { const waitForFunction = opts.waitForFunctionError ? vi.fn().mockRejectedValue(opts.waitForFunctionError) : vi.fn().mockResolvedValue(undefined); return { waitForFunction, locator: vi.fn(), } as unknown as Page; } /** Build a mock Locator whose evaluate() returns the given classList.has() result. */ function makeMockLocator(hasClass: boolean): Locator { return { evaluate: vi.fn().mockResolvedValue(hasClass), } as unknown as Locator; } /** Build a mock Locator whose evaluate() throws (simulates detached element). */ function makeMockLocatorThrows(): Locator { return { evaluate: vi.fn().mockRejectedValue(new Error("Element not attached")), } as unknown as Locator; } // --------------------------------------------------------------------------- beforeEach(() => { vi.clearAllMocks(); }); // --------------------------------------------------------------------------- describe("pw_assert_class", () => { // ------------------------------------------------------------------------- it("has class passes", async () => { // mustHave=true (default), element has the class → should resolve immediately. const page = makeMockPage({ waitForFunctionResolves: true }); const opts: PwAssertClassOptions = { selector: "[data-card-id='abc']", className: "border-red", mustHave: true, timeoutMs: 3000, }; await expect(pw_assert_class(page, opts)).resolves.toBeUndefined(); expect((page.waitForFunction as ReturnType)).toHaveBeenCalledOnce(); }); // ------------------------------------------------------------------------- it("lacks class passes", async () => { // mustHave=false, waitForFunction resolves → element does NOT have the class. const page = makeMockPage({ waitForFunctionResolves: true }); const opts: PwAssertClassOptions = { selector: ".card", className: "highlight-pulse", mustHave: false, timeoutMs: 3000, }; await expect(pw_assert_class(page, opts)).resolves.toBeUndefined(); }); // ------------------------------------------------------------------------- it("timeout error message", async () => { // waitForFunction throws a timeout-like error → pw_assert_class re-throws with detail. const page = makeMockPage({ waitForFunctionError: new Error("Timeout 3000ms exceeded"), }); const opts: PwAssertClassOptions = { selector: "[data-card-id='abc']", className: "border-red", mustHave: true, timeoutMs: 3000, }; await expect(pw_assert_class(page, opts)).rejects.toThrow( "expected [data-card-id='abc'] to have class border-red within 3000ms" ); }); // ------------------------------------------------------------------------- it("string selector resolves", async () => { // Verify the string path calls page.waitForFunction with the right serializable args. const page = makeMockPage({ waitForFunctionResolves: true }); const opts: PwAssertClassOptions = { selector: "div.card", className: "active", mustHave: true, timeoutMs: 5000, }; await pw_assert_class(page, opts); const wff = page.waitForFunction as ReturnType; expect(wff).toHaveBeenCalledOnce(); // Second arg must include sel, cls, must const serializedArg = wff.mock.calls[0][1] as { sel: string; cls: string; must: boolean }; expect(serializedArg).toMatchObject({ sel: "div.card", cls: "active", must: true }); // Third arg must include timeout const pwOpts = wff.mock.calls[0][2] as { timeout: number }; expect(pwOpts).toMatchObject({ timeout: 5000 }); }); // ------------------------------------------------------------------------- it("locator polled", async () => { // Locator path: evaluate returns true for mustHave=true → resolves without timeout. const locator = makeMockLocator(true); const page = makeMockPage(); const opts: PwAssertClassOptions = { selector: locator, className: "highlight-pulse", mustHave: true, timeoutMs: 1000, }; await expect(pw_assert_class(page, opts)).resolves.toBeUndefined(); // waitForFunction should NOT be called for Locator path expect((page.waitForFunction as ReturnType)).not.toHaveBeenCalled(); // evaluate called with className const evalMock = locator.evaluate as ReturnType; expect(evalMock).toHaveBeenCalledWith(expect.any(Function), "highlight-pulse"); }); // ------------------------------------------------------------------------- it("locator not have class passes", async () => { // mustHave=false, element lacks class (evaluate returns false) → condition met. const locator = makeMockLocator(false); const page = makeMockPage(); const opts: PwAssertClassOptions = { selector: locator, className: "border-red", mustHave: false, timeoutMs: 1000, }; await expect(pw_assert_class(page, opts)).resolves.toBeUndefined(); }); // ------------------------------------------------------------------------- it("locator timeout throws descriptive message", async () => { // evaluate always returns false for mustHave=true → exhausts timeout. const locator = makeMockLocator(false); const page = makeMockPage(); const opts: PwAssertClassOptions = { selector: locator, className: "border-red", mustHave: true, timeoutMs: 150, // Short timeout so test stays fast }; await expect(pw_assert_class(page, opts)).rejects.toThrow( "expected to have class border-red within 150ms" ); }); // ------------------------------------------------------------------------- it("locator evaluate throws treated as not met", async () => { // If evaluate throws (detached element), condition is treated as not met; // if that persists past timeout, final error is thrown. const locator = makeMockLocatorThrows(); const page = makeMockPage(); const opts: PwAssertClassOptions = { selector: locator, className: "border-red", mustHave: true, timeoutMs: 150, }; await expect(pw_assert_class(page, opts)).rejects.toThrow( "expected to have class border-red within 150ms" ); }); });