feat(registry): add playwright capability group (6 TS browser fns)
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>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: pw_assert_class
|
||||
kind: function
|
||||
lang: ts
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "async (page: Page, opts: PwAssertClassOptions) => Promise<void>"
|
||||
description: "Asserts a Playwright Locator has (or lacks) a given CSS class, with polling up to timeoutMs. Use for visual-state checks like red borders, highlight pulses, active tabs."
|
||||
tags: [playwright, e2e, browser, assert]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: page
|
||||
desc: "Playwright Page."
|
||||
- name: opts
|
||||
desc: "{selector, className, mustHave?, timeoutMs?}. selector accepts string CSS or Locator. className without leading dot."
|
||||
output: "void; throws with detailed message on timeout."
|
||||
tested: true
|
||||
tests: ["has class passes", "lacks class passes", "timeout error message", "string selector resolves", "locator polled"]
|
||||
test_file_path: "frontend/functions/browser/pw_assert_class.test.ts"
|
||||
file_path: "frontend/functions/browser/pw_assert_class.ts"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```typescript
|
||||
import { pw_assert_class } from "./pw_assert_class";
|
||||
|
||||
// Assert card has red border class after max-time exceeded
|
||||
await pw_assert_class(page, {
|
||||
selector: "[data-card-id='abc']",
|
||||
className: "border-red",
|
||||
mustHave: true,
|
||||
timeoutMs: 3000,
|
||||
});
|
||||
|
||||
// Assert roulette animation class is active on a card Locator
|
||||
const card = page.locator(".kanban-card").first();
|
||||
await pw_assert_class(page, {
|
||||
selector: card,
|
||||
className: "highlight-pulse",
|
||||
mustHave: true,
|
||||
timeoutMs: 2000,
|
||||
});
|
||||
|
||||
// Assert class is NOT present after animation ends
|
||||
await pw_assert_class(page, {
|
||||
selector: card,
|
||||
className: "highlight-pulse",
|
||||
mustHave: false,
|
||||
});
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites verificar que un elemento tiene (o no tiene) una clase CSS concreta en un test e2e de Playwright — por ejemplo comprobar que una tarjeta tiene `border-red` cuando se supera su tiempo máximo, o que `highlight-pulse` está activo durante la animación de ruleta. Úsala después de disparar la acción que debería cambiar el estado visual y antes de continuar con el siguiente paso del test.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Si `selector` es un string, usa `page.waitForFunction` internamente (delegado a la página), lo que es eficiente pero requiere que el selector sea un CSS selector válido para `document.querySelector`.
|
||||
- Si `selector` es un `Locator`, usa un bucle de polling con `locator.evaluate()`. Los Locators no se pueden serializar a través del contexto de `waitForFunction`.
|
||||
- `timeoutMs` por defecto es 5000 ms — ajústalo si la animación o transición tarda más.
|
||||
- Las clases se comprueban con `classList.contains(className)` — no incluyas el punto inicial.
|
||||
- Si el elemento está desconectado del DOM durante el polling de un Locator, se trata como condición no cumplida y sigue reintentando hasta el timeout.
|
||||
@@ -0,0 +1,187 @@
|
||||
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<typeof vi.fn>)).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<typeof vi.fn>;
|
||||
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<typeof vi.fn>)).not.toHaveBeenCalled();
|
||||
// evaluate called with className
|
||||
const evalMock = locator.evaluate as ReturnType<typeof vi.fn>;
|
||||
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 <Locator> 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 <Locator> to have class border-red within 150ms"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: pw_drag_drop
|
||||
kind: function
|
||||
lang: ts
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "async (page: Page, opts: PwDragDropOptions) => Promise<void>"
|
||||
description: "Simulates a human pointer drag in Playwright compatible with dnd-kit (PointerEvents + 8px activation threshold). Uses stepped mousemove with configurable pacing. Supports hoverMs for time-based dropzones (e.g. sidebar auto-open after 400ms hover)."
|
||||
tags: [playwright, e2e, browser, drag]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: page
|
||||
desc: "Playwright Page."
|
||||
- name: opts
|
||||
desc: "{source, target, steps?, delayMs?, hoverMs?, activateOffset?}. source/target accept selector string or Locator."
|
||||
output: "void; performs full pointer drag from source center to target center."
|
||||
tested: true
|
||||
tests: ["happy path", "no source bbox throws", "no target bbox throws", "hoverMs adds wait", "string selectors resolve"]
|
||||
test_file_path: "frontend/functions/browser/pw_drag_drop.test.ts"
|
||||
file_path: "frontend/functions/browser/pw_drag_drop.ts"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```typescript
|
||||
import { pw_drag_drop } from "./pw_drag_drop";
|
||||
|
||||
await pw_drag_drop(page, {
|
||||
source: "[data-card-id='abc']",
|
||||
target: "[data-column-id='xyz']",
|
||||
hoverMs: 500,
|
||||
});
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites arrastrar un elemento en el browser durante un test e2e con Playwright y el target usa `@dnd-kit/core`. El `dragTo` nativo de Playwright emite eventos HTML5 drag que dnd-kit ignora; esta funcion usa la API de mouse raw con un movimiento de activacion inicial para cruzar el umbral de 8px de dnd-kit.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `playwright` es una peer dependency: debe estar instalada en el proyecto consumidor (`pnpm add -D playwright`). El registry no la instala.
|
||||
- El umbral de activacion de dnd-kit es configurable en el `PointerSensor`; si el proyecto usa un umbral distinto al default de 8px, ajustar `activateOffset` (default: `{x: 12, y: 0}`) para superarlo.
|
||||
- `steps` y `delayMs` controlan la suavidad del drag. Valores bajos (steps=5, delayMs=0) pueden hacer que dnd-kit no detecte el movimiento como drag en versiones antiguas; los defaults (20 steps, 16ms) simulan ~60fps.
|
||||
- `hoverMs` es util para dropzones que se abren al mantener hover durante un tiempo (ej. sidebar de kanban que se abre tras 400ms). Pasarlo en 0 omite la espera.
|
||||
- Los source/target deben ser visibles y tener bounding box; si no se encuentran (display:none, detached) la funcion lanza un error descriptivo.
|
||||
- `activateOffset` es relativo al centro del source, no a su esquina.
|
||||
@@ -0,0 +1,153 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { pw_drag_drop } from "./pw_drag_drop";
|
||||
import type { Locator, Page } from "playwright";
|
||||
|
||||
// --- Mock factories ---
|
||||
|
||||
function makeLocator(bbox: { x: number; y: number; width: number; height: number } | null): Locator {
|
||||
return {
|
||||
boundingBox: vi.fn().mockResolvedValue(bbox),
|
||||
} as unknown as Locator;
|
||||
}
|
||||
|
||||
function makeFirstLocator(bbox: { x: number; y: number; width: number; height: number } | null): Locator {
|
||||
const loc = makeLocator(bbox);
|
||||
return loc;
|
||||
}
|
||||
|
||||
function makePage(
|
||||
sourceBbox: { x: number; y: number; width: number; height: number } | null,
|
||||
targetBbox: { x: number; y: number; width: number; height: number } | null,
|
||||
): Page {
|
||||
const moveStub = vi.fn().mockResolvedValue(undefined);
|
||||
const downStub = vi.fn().mockResolvedValue(undefined);
|
||||
const upStub = vi.fn().mockResolvedValue(undefined);
|
||||
const waitStub = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
// locator() returns an object with .first() that returns the locator mock
|
||||
const locatorFactory = (bbox: typeof sourceBbox) => {
|
||||
const inner = makeFirstLocator(bbox);
|
||||
return { first: vi.fn().mockReturnValue(inner) };
|
||||
};
|
||||
|
||||
let callCount = 0;
|
||||
const locatorMock = vi.fn().mockImplementation(() => {
|
||||
callCount++;
|
||||
if (callCount === 1) return locatorFactory(sourceBbox);
|
||||
return locatorFactory(targetBbox);
|
||||
});
|
||||
|
||||
return {
|
||||
mouse: {
|
||||
move: moveStub,
|
||||
down: downStub,
|
||||
up: upStub,
|
||||
},
|
||||
locator: locatorMock,
|
||||
waitForTimeout: waitStub,
|
||||
} as unknown as Page;
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
describe("pw_drag_drop", () => {
|
||||
describe("happy path", () => {
|
||||
it("calls mouse.down once, mouse.up once, mouse.move at least steps+2 times, waitForTimeout steps times when hoverMs=0", async () => {
|
||||
const steps = 5;
|
||||
const sourceBbox = { x: 10, y: 20, width: 100, height: 50 };
|
||||
const targetBbox = { x: 300, y: 20, width: 100, height: 50 };
|
||||
|
||||
const page = makePage(sourceBbox, targetBbox);
|
||||
const sourceLocator = makeLocator(sourceBbox);
|
||||
const targetLocator = makeLocator(targetBbox);
|
||||
|
||||
await pw_drag_drop(page, {
|
||||
source: sourceLocator,
|
||||
target: targetLocator,
|
||||
steps,
|
||||
delayMs: 0,
|
||||
hoverMs: 0,
|
||||
});
|
||||
|
||||
expect((page.mouse.down as ReturnType<typeof vi.fn>).mock.calls.length).toBe(1);
|
||||
expect((page.mouse.up as ReturnType<typeof vi.fn>).mock.calls.length).toBe(1);
|
||||
|
||||
// move(sx,sy) + move(activate, {steps:2}) + steps moves = at least steps + 2 calls
|
||||
const moveCalls = (page.mouse.move as ReturnType<typeof vi.fn>).mock.calls.length;
|
||||
expect(moveCalls).toBeGreaterThanOrEqual(steps + 2);
|
||||
|
||||
// waitForTimeout called exactly `steps` times (no hoverMs)
|
||||
const waitCalls = (page.waitForTimeout as ReturnType<typeof vi.fn>).mock.calls.length;
|
||||
expect(waitCalls).toBe(steps);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no source bbox throws", () => {
|
||||
it("throws when source.boundingBox returns null", async () => {
|
||||
const page = makePage(null, { x: 300, y: 20, width: 100, height: 50 });
|
||||
const sourceLocator = makeLocator(null);
|
||||
const targetLocator = makeLocator({ x: 300, y: 20, width: 100, height: 50 });
|
||||
|
||||
await expect(
|
||||
pw_drag_drop(page, { source: sourceLocator, target: targetLocator }),
|
||||
).rejects.toThrow("source element has no bounding box");
|
||||
});
|
||||
});
|
||||
|
||||
describe("no target bbox throws", () => {
|
||||
it("throws when target.boundingBox returns null", async () => {
|
||||
const page = makePage({ x: 10, y: 20, width: 100, height: 50 }, null);
|
||||
const sourceLocator = makeLocator({ x: 10, y: 20, width: 100, height: 50 });
|
||||
const targetLocator = makeLocator(null);
|
||||
|
||||
await expect(
|
||||
pw_drag_drop(page, { source: sourceLocator, target: targetLocator }),
|
||||
).rejects.toThrow("target element has no bounding box");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hoverMs adds wait", () => {
|
||||
it("calls waitForTimeout steps+1 times when hoverMs>0", async () => {
|
||||
const steps = 4;
|
||||
const sourceBbox = { x: 10, y: 20, width: 100, height: 50 };
|
||||
const targetBbox = { x: 300, y: 20, width: 100, height: 50 };
|
||||
|
||||
const page = makePage(sourceBbox, targetBbox);
|
||||
const sourceLocator = makeLocator(sourceBbox);
|
||||
const targetLocator = makeLocator(targetBbox);
|
||||
|
||||
await pw_drag_drop(page, {
|
||||
source: sourceLocator,
|
||||
target: targetLocator,
|
||||
steps,
|
||||
delayMs: 0,
|
||||
hoverMs: 400,
|
||||
});
|
||||
|
||||
const waitCalls = (page.waitForTimeout as ReturnType<typeof vi.fn>).mock.calls.length;
|
||||
// steps waits for delayMs + 1 wait for hoverMs
|
||||
expect(waitCalls).toBe(steps + 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("string selectors resolve", () => {
|
||||
it("calls page.locator(selector).first() when source/target are strings", async () => {
|
||||
const sourceBbox = { x: 10, y: 20, width: 100, height: 50 };
|
||||
const targetBbox = { x: 300, y: 20, width: 100, height: 50 };
|
||||
|
||||
const page = makePage(sourceBbox, targetBbox);
|
||||
|
||||
await pw_drag_drop(page, {
|
||||
source: "[data-card-id='abc']",
|
||||
target: "[data-column-id='xyz']",
|
||||
steps: 2,
|
||||
delayMs: 0,
|
||||
});
|
||||
|
||||
const locatorCalls = (page.locator as ReturnType<typeof vi.fn>).mock.calls;
|
||||
expect(locatorCalls.length).toBe(2);
|
||||
expect(locatorCalls[0]?.[0]).toBe("[data-card-id='abc']");
|
||||
expect(locatorCalls[1]?.[0]).toBe("[data-column-id='xyz']");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Locator, Page } from "playwright";
|
||||
|
||||
/** Options for pw_drag_drop. */
|
||||
export interface PwDragDropOptions {
|
||||
/** Selector string or Playwright Locator for the element to drag from. */
|
||||
source: string | Locator;
|
||||
/** Selector string or Playwright Locator for the drop target. */
|
||||
target: string | Locator;
|
||||
/** Number of intermediate mousemove steps from source to target. Default: 20. */
|
||||
steps?: number;
|
||||
/** Pause between each step in ms. Default: 16. */
|
||||
delayMs?: number;
|
||||
/** Pause over the target before releasing in ms. Useful for time-based dropzones. Default: 0. */
|
||||
hoverMs?: number;
|
||||
/** Initial offset after mousedown to cross dnd-kit's activation threshold (default 8px). Default: {x: 12, y: 0}. */
|
||||
activateOffset?: { x: number; y: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* pw_drag_drop — simulates a human pointer drag compatible with dnd-kit.
|
||||
*
|
||||
* Playwright's built-in dragTo uses HTML5 drag events which dnd-kit ignores
|
||||
* (it listens to PointerEvents). This helper uses raw mouse API with a
|
||||
* small activation move to cross dnd-kit's default 8px threshold, then
|
||||
* interpolates in configurable steps toward the target.
|
||||
*/
|
||||
export async function pw_drag_drop(page: Page, opts: PwDragDropOptions): Promise<void> {
|
||||
const steps = opts.steps ?? 20;
|
||||
const delayMs = opts.delayMs ?? 16;
|
||||
const hoverMs = opts.hoverMs ?? 0;
|
||||
const activateOffset = opts.activateOffset ?? { x: 12, y: 0 };
|
||||
|
||||
// Resolve source and target to Locators.
|
||||
const sourceLocator: Locator =
|
||||
typeof opts.source === "string" ? page.locator(opts.source).first() : opts.source;
|
||||
const targetLocator: Locator =
|
||||
typeof opts.target === "string" ? page.locator(opts.target).first() : opts.target;
|
||||
|
||||
// Get bounding boxes.
|
||||
const sourceBbox = await sourceLocator.boundingBox();
|
||||
if (sourceBbox === null) {
|
||||
throw new Error("pw_drag_drop: source element has no bounding box (not visible or detached)");
|
||||
}
|
||||
|
||||
const targetBbox = await targetLocator.boundingBox();
|
||||
if (targetBbox === null) {
|
||||
throw new Error("pw_drag_drop: target element has no bounding box (not visible or detached)");
|
||||
}
|
||||
|
||||
// Compute centers.
|
||||
const sx = sourceBbox.x + sourceBbox.width / 2;
|
||||
const sy = sourceBbox.y + sourceBbox.height / 2;
|
||||
const tx = targetBbox.x + targetBbox.width / 2;
|
||||
const ty = targetBbox.y + targetBbox.height / 2;
|
||||
|
||||
// Move to source and press down.
|
||||
await page.mouse.move(sx, sy);
|
||||
await page.mouse.down();
|
||||
|
||||
// Small move to cross dnd-kit activation threshold (default 8px).
|
||||
await page.mouse.move(sx + activateOffset.x, sy + activateOffset.y, { steps: 2 });
|
||||
|
||||
// Interpolate from activation position toward target.
|
||||
const startX = sx + activateOffset.x;
|
||||
const startY = sy + activateOffset.y;
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const xi = startX + (tx - startX) * t;
|
||||
const yi = startY + (ty - startY) * t;
|
||||
await page.mouse.move(xi, yi);
|
||||
await page.waitForTimeout(delayMs);
|
||||
}
|
||||
|
||||
// Optional hover pause before release (useful for sidebar auto-open, etc.).
|
||||
if (hoverMs > 0) {
|
||||
await page.waitForTimeout(hoverMs);
|
||||
}
|
||||
|
||||
// Release.
|
||||
await page.mouse.up();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: pw_kanban_login
|
||||
kind: function
|
||||
lang: ts
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "async (page: Page, opts: PwLoginOptions) => Promise<void>"
|
||||
description: "Authenticates a Playwright Page against the kanban backend via POST /api/login. Sets session cookie and navigates to root. Throws on failure or if login page remains visible."
|
||||
tags: [playwright, e2e, browser, kanban]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: page
|
||||
desc: "Playwright Page (typically from pw_launch_browser). The page context shares storageState with page.request, so the Set-Cookie response propagates automatically."
|
||||
- name: opts
|
||||
desc: "{username, password, baseUrl?}. baseUrl defaults to the origin derived from page.url() — so the page must already be navigated to the target host before calling this function."
|
||||
output: "void; throws on failed login (HTTP >= 400) or if login page selectors remain visible after navigation. After resolve, page is on root (baseUrl + '/') and the kanban_session cookie is active."
|
||||
tested: true
|
||||
tests:
|
||||
- "happy path"
|
||||
- "rejects 401"
|
||||
- "rejects when login page still visible"
|
||||
test_file_path: "frontend/functions/browser/pw_kanban_login.test.ts"
|
||||
file_path: "frontend/functions/browser/pw_kanban_login.ts"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```typescript
|
||||
import { pw_launch_browser } from "@fn_library/../browser/pw_launch_browser";
|
||||
import { pw_kanban_login } from "@fn_library/../browser/pw_kanban_login";
|
||||
|
||||
const { page, close } = await pw_launch_browser({ baseUrl: "http://localhost:5180" });
|
||||
await pw_kanban_login(page, { username: "egutierrez", password: "..." });
|
||||
// page is now on http://localhost:5180/ with kanban_session cookie active
|
||||
await close();
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usala al inicio de cualquier test Playwright o script e2e contra el kanban. Despues de esta llamada el `page` esta autenticado y puede acceder a rutas protegidas sin redireccion al login.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `page.url()` debe devolver una URL valida con origin antes de llamar a esta funcion si no se pasa `baseUrl`. Si la pagina no ha navegado todavia (`about:blank`), pasar `baseUrl` explicitamente.
|
||||
- La funcion asume que `page.request` comparte el contexto de cookies con `page` (comportamiento por defecto de Playwright). Si usas un `APIRequestContext` separado, la cookie no se propagara automaticamente.
|
||||
- La comprobacion post-login busca texto literal "Login" o "Iniciar sesion". Si el backend usa otro texto en el boton, la comprobacion pasara aunque la autenticacion haya fallado silenciosamente — en ese caso ampliar los selectores de sanidad.
|
||||
- Espera `networkidle` tras `goto`: si la app lanza requests continuos (polling, WebSocket upgrade), `networkidle` puede tardar o no resolverse. Considerar `page.waitForURL` como alternativa en esos casos.
|
||||
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { pw_kanban_login } from "./pw_kanban_login";
|
||||
|
||||
// --- mock helpers ---
|
||||
|
||||
function makeResponse(status: number) {
|
||||
return { status: () => status };
|
||||
}
|
||||
|
||||
function makePage(overrides: {
|
||||
currentUrl?: string;
|
||||
postStatus?: number;
|
||||
loginTextVisible?: boolean;
|
||||
} = {}) {
|
||||
const currentUrl = overrides.currentUrl ?? "http://localhost:5180/login";
|
||||
const postStatus = overrides.postStatus ?? 200;
|
||||
const loginTextVisible = overrides.loginTextVisible ?? false;
|
||||
|
||||
const locatorMock = {
|
||||
isVisible: vi.fn().mockResolvedValue(loginTextVisible),
|
||||
};
|
||||
|
||||
return {
|
||||
url: vi.fn().mockReturnValue(currentUrl),
|
||||
request: {
|
||||
post: vi.fn().mockResolvedValue(makeResponse(postStatus)),
|
||||
},
|
||||
goto: vi.fn().mockResolvedValue(undefined),
|
||||
waitForLoadState: vi.fn().mockResolvedValue(undefined),
|
||||
waitForSelector: vi.fn().mockResolvedValue(undefined),
|
||||
locator: vi.fn().mockReturnValue(locatorMock),
|
||||
_locatorMock: locatorMock,
|
||||
};
|
||||
}
|
||||
|
||||
// --- tests ---
|
||||
|
||||
describe("pw_kanban_login", () => {
|
||||
it("happy path", async () => {
|
||||
const page = makePage({ postStatus: 200, loginTextVisible: false });
|
||||
|
||||
await pw_kanban_login(page as any, {
|
||||
username: "egutierrez",
|
||||
password: "secret",
|
||||
});
|
||||
|
||||
// Should POST to derived origin + /api/login with correct body
|
||||
expect(page.request.post).toHaveBeenCalledWith(
|
||||
"http://localhost:5180/api/login",
|
||||
{ data: { username: "egutierrez", password: "secret" } },
|
||||
);
|
||||
|
||||
// Should navigate to root
|
||||
expect(page.goto).toHaveBeenCalledWith("http://localhost:5180/");
|
||||
|
||||
// Should wait for page load
|
||||
expect(page.waitForLoadState).toHaveBeenCalledWith("networkidle");
|
||||
expect(page.waitForSelector).toHaveBeenCalledWith("body", { state: "attached" });
|
||||
});
|
||||
|
||||
it("rejects 401", async () => {
|
||||
const page = makePage({ postStatus: 401 });
|
||||
|
||||
await expect(
|
||||
pw_kanban_login(page as any, { username: "bad", password: "wrong" }),
|
||||
).rejects.toThrow("kanban login failed: 401");
|
||||
|
||||
// Should NOT navigate after failed login
|
||||
expect(page.goto).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects 500", async () => {
|
||||
const page = makePage({ postStatus: 500 });
|
||||
|
||||
await expect(
|
||||
pw_kanban_login(page as any, { username: "u", password: "p" }),
|
||||
).rejects.toThrow("kanban login failed: 500");
|
||||
});
|
||||
|
||||
it("rejects when login page still visible", async () => {
|
||||
// Simulate server returns 200 but login page is still showing (e.g. cookie not set)
|
||||
const page = makePage({ postStatus: 200, loginTextVisible: true });
|
||||
|
||||
await expect(
|
||||
pw_kanban_login(page as any, { username: "egutierrez", password: "secret" }),
|
||||
).rejects.toThrow("kanban login failed: login page still visible after authentication");
|
||||
});
|
||||
|
||||
it("uses explicit baseUrl over page origin", async () => {
|
||||
const page = makePage({ currentUrl: "http://localhost:5180/login", postStatus: 200 });
|
||||
|
||||
await pw_kanban_login(page as any, {
|
||||
username: "egutierrez",
|
||||
password: "secret",
|
||||
baseUrl: "http://custom-host:9000",
|
||||
});
|
||||
|
||||
expect(page.request.post).toHaveBeenCalledWith(
|
||||
"http://custom-host:9000/api/login",
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(page.goto).toHaveBeenCalledWith("http://custom-host:9000/");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { Page } from "playwright";
|
||||
|
||||
/** Options for authenticating a Playwright Page against the kanban backend. */
|
||||
export interface PwLoginOptions {
|
||||
username: string;
|
||||
password: string;
|
||||
/** Base URL of the kanban server. Defaults to the origin of page.url(). */
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* pw_kanban_login — authenticates a Playwright Page against the kanban backend.
|
||||
*
|
||||
* Posts credentials to POST /api/auth/login. The request fixture shares storageState
|
||||
* with the page context, so the Set-Cookie: kanban_session cookie auto-propagates.
|
||||
* After successful login, navigates to root and verifies the login page is gone.
|
||||
*
|
||||
* Throws if the login response returns HTTP >= 400 or if the login page remains
|
||||
* visible after navigation.
|
||||
*/
|
||||
export async function pw_kanban_login(page: Page, opts: PwLoginOptions): Promise<void> {
|
||||
const { username, password } = opts;
|
||||
|
||||
// Resolve baseUrl from opts or from current page origin
|
||||
const baseUrl = opts.baseUrl ?? new URL(page.url()).origin;
|
||||
|
||||
// POST credentials — page.request shares cookies/storageState with the page context
|
||||
const response = await page.request.post(`${baseUrl}/api/auth/login`, {
|
||||
data: { username, password },
|
||||
});
|
||||
|
||||
if (response.status() >= 400) {
|
||||
throw new Error(`kanban login failed: ${response.status()}`);
|
||||
}
|
||||
|
||||
// Navigate to root; cookie is already propagated via shared context
|
||||
await page.goto(`${baseUrl}/`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForSelector("body", { state: "attached" });
|
||||
|
||||
// Sanity check: ensure we are no longer on the login page
|
||||
const loginTextVisible =
|
||||
(await page.locator("text=Login").isVisible()) ||
|
||||
(await page.locator("text=Iniciar sesión").isVisible());
|
||||
|
||||
if (loginTextVisible) {
|
||||
throw new Error("kanban login failed: login page still visible after authentication");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: pw_keyboard_sequence
|
||||
kind: function
|
||||
lang: ts
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "async (page: Page, sequence: KbStep[]) => Promise<void>"
|
||||
description: "Executes a sequence of keyboard interactions (focus, type, key press, wait) on a Playwright Page. Use to script realistic input flows like typing then navigating autocomplete dropdowns with arrow keys."
|
||||
tags: ["playwright", "e2e", "browser", "keyboard"]
|
||||
params:
|
||||
- name: page
|
||||
desc: "Playwright Page."
|
||||
- name: sequence
|
||||
desc: "Array of steps: focus/type/press/wait. Executed in order."
|
||||
output: "void; mutates focused element / page state."
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["playwright"]
|
||||
tested: true
|
||||
tests:
|
||||
- "focus calls focus"
|
||||
- "type uses default delay"
|
||||
- "press routes correctly"
|
||||
- "wait calls waitForTimeout"
|
||||
- "unknown kind throws"
|
||||
- "order preserved"
|
||||
test_file_path: "frontend/functions/browser/pw_keyboard_sequence.test.ts"
|
||||
file_path: "frontend/functions/browser/pw_keyboard_sequence.ts"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```ts
|
||||
import { pw_keyboard_sequence } from "./pw_keyboard_sequence";
|
||||
|
||||
await pw_keyboard_sequence(page, [
|
||||
{ kind: "focus", selector: "input[name=requester]" },
|
||||
{ kind: "type", text: "Enma" },
|
||||
{ kind: "wait", ms: 200 },
|
||||
{ kind: "press", key: "ArrowDown" },
|
||||
{ kind: "press", key: "ArrowDown" },
|
||||
{ kind: "press", key: "Enter" },
|
||||
]);
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites encadenar foco + escritura + teclas especiales sobre un elemento (autocompletado, selects, modales de búsqueda). Úsala en tests E2E con Playwright en lugar de mezclar calls sueltas de `keyboard.type` y `keyboard.press`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `focus` usa `.first()` — si el selector coincide con múltiples elementos, foca el primero. Afinar el selector si es necesario.
|
||||
- `type` simula escritura carácter a carácter con `delay` (default 30 ms). Para input más rápido pasar `delayMs: 0`.
|
||||
- `press` espera un nombre de tecla Playwright válido: `"ArrowDown"`, `"Enter"`, `"Escape"`, `"Tab"`, etc.
|
||||
- Un `kind` desconocido lanza en runtime (útil para detectar typos en JS sin TypeScript).
|
||||
@@ -0,0 +1,106 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { Page } from "playwright";
|
||||
import { pw_keyboard_sequence, type KbStep } from "./pw_keyboard_sequence";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockFocus = vi.fn().mockResolvedValue(undefined);
|
||||
const mockFirst = vi.fn().mockReturnValue({ focus: mockFocus });
|
||||
const mockLocator = vi.fn().mockReturnValue({ first: mockFirst });
|
||||
const mockType = vi.fn().mockResolvedValue(undefined);
|
||||
const mockPress = vi.fn().mockResolvedValue(undefined);
|
||||
const mockWaitForTimeout = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const mockPage = {
|
||||
locator: mockLocator,
|
||||
keyboard: {
|
||||
type: mockType,
|
||||
press: mockPress,
|
||||
},
|
||||
waitForTimeout: mockWaitForTimeout,
|
||||
} as unknown as Page;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockFirst.mockReturnValue({ focus: mockFocus });
|
||||
mockLocator.mockReturnValue({ first: mockFirst });
|
||||
mockFocus.mockResolvedValue(undefined);
|
||||
mockType.mockResolvedValue(undefined);
|
||||
mockPress.mockResolvedValue(undefined);
|
||||
mockWaitForTimeout.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("pw_keyboard_sequence", () => {
|
||||
it("focus calls focus", async () => {
|
||||
const sequence: KbStep[] = [{ kind: "focus", selector: "input[name=requester]" }];
|
||||
await pw_keyboard_sequence(mockPage, sequence);
|
||||
|
||||
expect(mockLocator).toHaveBeenCalledWith("input[name=requester]");
|
||||
expect(mockFirst).toHaveBeenCalledOnce();
|
||||
expect(mockFocus).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("type uses default delay", async () => {
|
||||
const sequence: KbStep[] = [{ kind: "type", text: "Enmanuel" }];
|
||||
await pw_keyboard_sequence(mockPage, sequence);
|
||||
|
||||
expect(mockType).toHaveBeenCalledWith("Enmanuel", { delay: 30 });
|
||||
});
|
||||
|
||||
it("type with explicit delayMs uses that value", async () => {
|
||||
const sequence: KbStep[] = [{ kind: "type", text: "hello", delayMs: 100 }];
|
||||
await pw_keyboard_sequence(mockPage, sequence);
|
||||
|
||||
expect(mockType).toHaveBeenCalledWith("hello", { delay: 100 });
|
||||
});
|
||||
|
||||
it("press routes correctly", async () => {
|
||||
const sequence: KbStep[] = [
|
||||
{ kind: "press", key: "ArrowDown" },
|
||||
{ kind: "press", key: "Enter" },
|
||||
];
|
||||
await pw_keyboard_sequence(mockPage, sequence);
|
||||
|
||||
expect(mockPress).toHaveBeenCalledTimes(2);
|
||||
expect(mockPress).toHaveBeenNthCalledWith(1, "ArrowDown");
|
||||
expect(mockPress).toHaveBeenNthCalledWith(2, "Enter");
|
||||
});
|
||||
|
||||
it("wait calls waitForTimeout", async () => {
|
||||
const sequence: KbStep[] = [{ kind: "wait", ms: 200 }];
|
||||
await pw_keyboard_sequence(mockPage, sequence);
|
||||
|
||||
expect(mockWaitForTimeout).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
it("unknown kind throws", async () => {
|
||||
const sequence = [{ kind: "unknown_step" }] as unknown as KbStep[];
|
||||
await expect(pw_keyboard_sequence(mockPage, sequence)).rejects.toThrow(
|
||||
'pw_keyboard_sequence: unknown step kind "unknown_step"'
|
||||
);
|
||||
});
|
||||
|
||||
it("order preserved", async () => {
|
||||
const callOrder: string[] = [];
|
||||
|
||||
mockFocus.mockImplementationOnce(async () => { callOrder.push("focus"); });
|
||||
mockType.mockImplementationOnce(async () => { callOrder.push("type"); });
|
||||
mockWaitForTimeout.mockImplementationOnce(async () => { callOrder.push("wait"); });
|
||||
mockPress.mockImplementationOnce(async () => { callOrder.push("press"); });
|
||||
|
||||
const sequence: KbStep[] = [
|
||||
{ kind: "focus", selector: "input" },
|
||||
{ kind: "type", text: "abc" },
|
||||
{ kind: "wait", ms: 50 },
|
||||
{ kind: "press", key: "Enter" },
|
||||
];
|
||||
|
||||
await pw_keyboard_sequence(mockPage, sequence);
|
||||
|
||||
expect(callOrder).toEqual(["focus", "type", "wait", "press"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Page } from "playwright";
|
||||
|
||||
/**
|
||||
* A single step in a keyboard interaction sequence.
|
||||
*
|
||||
* - `focus` — focuses a DOM element via CSS selector.
|
||||
* - `type` — types text character by character with a humanlike delay.
|
||||
* - `press` — sends a single named key (e.g. "ArrowDown", "Enter", "Escape", "Tab").
|
||||
* - `wait` — pauses execution for the given number of milliseconds.
|
||||
*/
|
||||
export type KbStep =
|
||||
| { kind: "focus"; selector: string }
|
||||
| { kind: "type"; text: string; delayMs?: number }
|
||||
| { kind: "press"; key: string }
|
||||
| { kind: "wait"; ms: number };
|
||||
|
||||
/**
|
||||
* pw_keyboard_sequence — executes an ordered sequence of keyboard interactions on a Playwright Page.
|
||||
*
|
||||
* Use to script realistic input flows such as typing into an autocomplete field and then
|
||||
* navigating its dropdown with arrow keys before pressing Enter.
|
||||
*/
|
||||
export async function pw_keyboard_sequence(
|
||||
page: Page,
|
||||
sequence: KbStep[]
|
||||
): Promise<void> {
|
||||
for (const step of sequence) {
|
||||
switch (step.kind) {
|
||||
case "focus":
|
||||
await page.locator(step.selector).first().focus();
|
||||
break;
|
||||
case "type":
|
||||
await page.keyboard.type(step.text, { delay: step.delayMs ?? 30 });
|
||||
break;
|
||||
case "press":
|
||||
await page.keyboard.press(step.key);
|
||||
break;
|
||||
case "wait":
|
||||
await page.waitForTimeout(step.ms);
|
||||
break;
|
||||
default: {
|
||||
// TypeScript exhaustiveness check — will also throw at runtime for JS callers.
|
||||
const _exhaustive: never = step;
|
||||
throw new Error(
|
||||
`pw_keyboard_sequence: unknown step kind "${(step as KbStep & { kind: string }).kind}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: pw_launch_browser
|
||||
kind: function
|
||||
lang: ts
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "async (opts?: PwLaunchOptions) => Promise<PwHandles>"
|
||||
description: "Launches chromium and returns a Page navigated to baseUrl. Optionally pre-authenticates via storageState. Used as the entry point for any Playwright e2e test in the registry."
|
||||
tags: [playwright, e2e, browser]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: opts
|
||||
desc: "Launch options; all fields optional with sensible defaults for kanban e2e."
|
||||
output: "Playwright handles {browser, context, page, close}; page is already navigated to baseUrl."
|
||||
tested: true
|
||||
tests: ["launch defaults", "launch with storageState", "close calls both"]
|
||||
test_file_path: "frontend/functions/browser/pw_launch_browser.test.ts"
|
||||
file_path: "frontend/functions/browser/pw_launch_browser.ts"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```typescript
|
||||
import { pw_launch_browser } from "./pw_launch_browser";
|
||||
|
||||
const h = await pw_launch_browser({ baseUrl: "http://localhost:5180", headless: false });
|
||||
await h.page.click("text=Login");
|
||||
await h.close();
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites iniciar un test e2e de Playwright para el SPA de kanban (o cualquier frontend del registry). Es el primer paso de cualquier suite: abre el browser, navega al baseUrl y devuelve los handles listos para interactuar.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `playwright` es una peer dependency: debe estar instalada en el proyecto consumidor (`pnpm add -D playwright` o `npm i -D playwright`). El registry no la instala.
|
||||
- `storageStatePath` se ignora silenciosamente si el archivo no existe — verifica que el archivo de auth se haya generado antes de pasarlo.
|
||||
- `close()` cierra context primero y luego browser; no llames `browser.close()` manualmente antes de `close()` o el context quedara huerfano.
|
||||
- La navegacion a `baseUrl` ocurre dentro de `pw_launch_browser`; si el servidor no esta levantado, `page.goto` lanzara un error de red.
|
||||
@@ -0,0 +1,121 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// --- mocks ----------------------------------------------------------------
|
||||
|
||||
const mockPage = {
|
||||
goto: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const mockContext = {
|
||||
newPage: vi.fn().mockResolvedValue(mockPage),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const mockBrowser = {
|
||||
newContext: vi.fn().mockResolvedValue(mockContext),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const mockChromium = {
|
||||
launch: vi.fn().mockResolvedValue(mockBrowser),
|
||||
};
|
||||
|
||||
vi.mock("playwright", () => ({
|
||||
chromium: mockChromium,
|
||||
}));
|
||||
|
||||
// Mock fs.existsSync — return true by default; individual tests override as needed.
|
||||
vi.mock("fs", () => ({
|
||||
existsSync: vi.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
import { existsSync } from "fs";
|
||||
import { pw_launch_browser } from "./pw_launch_browser";
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset page.goto and existsSync defaults after clearing
|
||||
mockPage.goto.mockResolvedValue(undefined);
|
||||
mockContext.newPage.mockResolvedValue(mockPage);
|
||||
mockContext.close.mockResolvedValue(undefined);
|
||||
mockBrowser.newContext.mockResolvedValue(mockContext);
|
||||
mockBrowser.close.mockResolvedValue(undefined);
|
||||
mockChromium.launch.mockResolvedValue(mockBrowser);
|
||||
vi.mocked(existsSync).mockReturnValue(true);
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
describe("pw_launch_browser", () => {
|
||||
it("launch defaults", async () => {
|
||||
const handles = await pw_launch_browser();
|
||||
|
||||
// chromium.launch called with defaults
|
||||
expect(mockChromium.launch).toHaveBeenCalledWith({ headless: true, slowMo: 0 });
|
||||
|
||||
// context created with default viewport
|
||||
expect(mockBrowser.newContext).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ viewport: { width: 1280, height: 800 } })
|
||||
);
|
||||
|
||||
// no storageState in context options when not provided
|
||||
const ctxCall = mockBrowser.newContext.mock.calls[0][0] as Record<string, unknown>;
|
||||
expect(ctxCall.storageState).toBeUndefined();
|
||||
|
||||
// page navigated to default baseUrl
|
||||
expect(mockPage.goto).toHaveBeenCalledWith("http://localhost:5180");
|
||||
|
||||
// handles returned
|
||||
expect(handles.browser).toBe(mockBrowser);
|
||||
expect(handles.context).toBe(mockContext);
|
||||
expect(handles.page).toBe(mockPage);
|
||||
expect(typeof handles.close).toBe("function");
|
||||
});
|
||||
|
||||
it("launch with storageState", async () => {
|
||||
vi.mocked(existsSync).mockReturnValue(true);
|
||||
|
||||
const handles = await pw_launch_browser({
|
||||
headless: false,
|
||||
baseUrl: "http://localhost:5173",
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
storageStatePath: "/tmp/auth.json",
|
||||
slowMo: 50,
|
||||
});
|
||||
|
||||
expect(mockChromium.launch).toHaveBeenCalledWith({ headless: false, slowMo: 50 });
|
||||
expect(mockBrowser.newContext).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
storageState: "/tmp/auth.json",
|
||||
})
|
||||
);
|
||||
expect(mockPage.goto).toHaveBeenCalledWith("http://localhost:5173");
|
||||
expect(handles.page).toBe(mockPage);
|
||||
});
|
||||
|
||||
it("close calls both", async () => {
|
||||
const handles = await pw_launch_browser();
|
||||
|
||||
await handles.close();
|
||||
|
||||
expect(mockContext.close).toHaveBeenCalledOnce();
|
||||
expect(mockBrowser.close).toHaveBeenCalledOnce();
|
||||
|
||||
// context must close before browser (call order)
|
||||
const contextCloseOrder = mockContext.close.mock.invocationCallOrder[0];
|
||||
const browserCloseOrder = mockBrowser.close.mock.invocationCallOrder[0];
|
||||
expect(contextCloseOrder).toBeLessThan(browserCloseOrder);
|
||||
});
|
||||
|
||||
it("storageState skipped when file missing", async () => {
|
||||
vi.mocked(existsSync).mockReturnValue(false);
|
||||
|
||||
await pw_launch_browser({ storageStatePath: "/tmp/nonexistent.json" });
|
||||
|
||||
const ctxCall = mockBrowser.newContext.mock.calls[0][0] as Record<string, unknown>;
|
||||
expect(ctxCall.storageState).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { existsSync } from "fs";
|
||||
import type { Browser, BrowserContext, Page } from "playwright";
|
||||
|
||||
/** Options for launching a Playwright browser session. */
|
||||
export interface PwLaunchOptions {
|
||||
/** Use headless mode. Default: true */
|
||||
headless?: boolean;
|
||||
/** Base URL to navigate to after opening the page. Default: "http://localhost:5180" */
|
||||
baseUrl?: string;
|
||||
/** Viewport size. Default: 1280x800 */
|
||||
viewport?: { width: number; height: number };
|
||||
/** Optional path to a Playwright storageState JSON for pre-auth. Loaded only if the file exists. */
|
||||
storageStatePath?: string;
|
||||
/** Slow down operations by the given ms. Default: 0 */
|
||||
slowMo?: number;
|
||||
}
|
||||
|
||||
/** Playwright handles returned after launching the browser. */
|
||||
export interface PwHandles {
|
||||
browser: Browser;
|
||||
context: BrowserContext;
|
||||
page: Page;
|
||||
/** Closes context then browser. */
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* pw_launch_browser — launches chromium and returns a Page navigated to baseUrl.
|
||||
*
|
||||
* Optionally pre-authenticates via storageState (loaded only if the file exists).
|
||||
* Returns handles including a convenience close() that tears down context and browser.
|
||||
*/
|
||||
export async function pw_launch_browser(opts: PwLaunchOptions = {}): Promise<PwHandles> {
|
||||
const { chromium } = await import("playwright");
|
||||
|
||||
const headless = opts.headless ?? true;
|
||||
const slowMo = opts.slowMo ?? 0;
|
||||
const baseUrl = opts.baseUrl ?? "http://localhost:5180";
|
||||
const viewport = opts.viewport ?? { width: 1280, height: 800 };
|
||||
|
||||
const browser = await chromium.launch({ headless, slowMo });
|
||||
|
||||
const contextOptions: Parameters<Browser["newContext"]>[0] = { viewport };
|
||||
if (opts.storageStatePath && existsSync(opts.storageStatePath)) {
|
||||
contextOptions.storageState = opts.storageStatePath;
|
||||
}
|
||||
|
||||
const context = await browser.newContext(contextOptions);
|
||||
const page = await context.newPage();
|
||||
await page.goto(baseUrl);
|
||||
|
||||
const close = async (): Promise<void> => {
|
||||
await context.close();
|
||||
await browser.close();
|
||||
};
|
||||
|
||||
return { browser, context, page, close };
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: pw_wait_predicate
|
||||
kind: function
|
||||
lang: ts
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "async <T = unknown>(page: Page, predicate: string | ((arg?: unknown) => unknown), opts?: PwWaitOptions) => Promise<T>"
|
||||
description: "Polls an arbitrary predicate inside the page context until it returns truthy or times out. Thin wrapper around page.waitForFunction with friendlier defaults and custom error messages. Use to wait for CSS class changes, DOM counts, or any computed condition."
|
||||
tags: [playwright, e2e, browser, wait]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: page
|
||||
desc: "Playwright Page."
|
||||
- name: predicate
|
||||
desc: "JS expression as string OR function evaluated in page context. Returns truthy to stop polling."
|
||||
- name: opts
|
||||
desc: "{timeoutMs?, pollMs?, arg?, message?}."
|
||||
output: "Resolved value of the predicate (page.waitForFunction jsonValue)."
|
||||
tested: true
|
||||
tests: ["defaults applied", "arg passed through", "jsonValue returned", "message wraps timeout"]
|
||||
test_file_path: "frontend/functions/browser/pw_wait_predicate.test.ts"
|
||||
file_path: "frontend/functions/browser/pw_wait_predicate.ts"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```typescript
|
||||
import { pw_wait_predicate } from "./pw_wait_predicate";
|
||||
|
||||
// Wait for at least one red-bordered card to appear
|
||||
await pw_wait_predicate(
|
||||
page,
|
||||
() => document.querySelectorAll('[data-card-id].border-red').length > 0,
|
||||
{ timeoutMs: 2000, message: "no red-bordered card appeared" },
|
||||
);
|
||||
|
||||
// Wait for exactly 1 highlighted card using an arg
|
||||
await pw_wait_predicate(
|
||||
page,
|
||||
(sel) => document.querySelectorAll(sel as string).length === 1,
|
||||
{ arg: ".highlighted", timeoutMs: 3000, pollMs: 50 },
|
||||
);
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando `waitForSelector` no basta porque la condicion depende de una clase CSS compuesta, un conteo exacto de elementos, o cualquier expresion calculada en el contexto de la pagina. Usar antes de assertions que requieren un estado DOM especifico que tarda en estabilizarse.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `predicate` se ejecuta en el contexto de la pagina (sandbox del browser), no en Node. No puede cerrar sobre variables de Node; usa `opts.arg` para pasar datos.
|
||||
- Si `predicate` es una funcion, Playwright la serializa a string para enviarla al browser. No uses referencias externas ni closures de Node dentro de ella.
|
||||
- `opts.message` solo se usa cuando `waitForFunction` lanza; no modifica el valor de retorno en caso de exito.
|
||||
- `playwright` debe estar instalada en el proyecto consumidor como peer dependency. El registry no la instala.
|
||||
- Con `polling: number`, Playwright hace polling activo (busy loop con sleeps cortos). Un `pollMs` muy bajo (< 50) puede saturar la CPU del browser en tests largos.
|
||||
@@ -0,0 +1,88 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { JSHandle } from "playwright";
|
||||
|
||||
// --- mocks ------------------------------------------------------------------
|
||||
|
||||
const mockJsonValue = vi.fn();
|
||||
|
||||
const mockHandle: Partial<JSHandle> = {
|
||||
jsonValue: mockJsonValue,
|
||||
};
|
||||
|
||||
const mockPage = {
|
||||
waitForFunction: vi.fn(),
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockPage.waitForFunction.mockResolvedValue(mockHandle);
|
||||
mockJsonValue.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
import { pw_wait_predicate } from "./pw_wait_predicate";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("pw_wait_predicate", () => {
|
||||
it("defaults applied", async () => {
|
||||
const predicate = () => document.querySelector(".ready") !== null;
|
||||
|
||||
await pw_wait_predicate(mockPage as never, predicate);
|
||||
|
||||
expect(mockPage.waitForFunction).toHaveBeenCalledWith(
|
||||
predicate,
|
||||
undefined, // arg default
|
||||
{ timeout: 5000, polling: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
it("arg passed through", async () => {
|
||||
const predicate = (sel: unknown) =>
|
||||
document.querySelectorAll(sel as string).length > 0;
|
||||
const arg = ".border-red";
|
||||
|
||||
await pw_wait_predicate(mockPage as never, predicate, {
|
||||
timeoutMs: 2000,
|
||||
pollMs: 50,
|
||||
arg,
|
||||
});
|
||||
|
||||
expect(mockPage.waitForFunction).toHaveBeenCalledWith(
|
||||
predicate,
|
||||
arg,
|
||||
{ timeout: 2000, polling: 50 },
|
||||
);
|
||||
});
|
||||
|
||||
it("jsonValue returned", async () => {
|
||||
const expected = { count: 3 };
|
||||
mockJsonValue.mockResolvedValue(expected);
|
||||
|
||||
const result = await pw_wait_predicate(mockPage as never, "() => true");
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
expect(mockJsonValue).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("message wraps timeout", async () => {
|
||||
const originalError = new Error("Timeout 5000ms exceeded");
|
||||
mockPage.waitForFunction.mockRejectedValue(originalError);
|
||||
|
||||
await expect(
|
||||
pw_wait_predicate(mockPage as never, "() => false", {
|
||||
message: "no red-bordered card appeared",
|
||||
}),
|
||||
).rejects.toThrow("no red-bordered card appeared: Timeout 5000ms exceeded");
|
||||
});
|
||||
|
||||
it("error passthrough without message", async () => {
|
||||
const originalError = new Error("Something went wrong");
|
||||
mockPage.waitForFunction.mockRejectedValue(originalError);
|
||||
|
||||
await expect(
|
||||
pw_wait_predicate(mockPage as never, "() => false"),
|
||||
).rejects.toThrow("Something went wrong");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Page } from "playwright";
|
||||
|
||||
/** Options for pw_wait_predicate. */
|
||||
export interface PwWaitOptions {
|
||||
/** Maximum time to wait in milliseconds. Default: 5000 */
|
||||
timeoutMs?: number;
|
||||
/** Polling interval in milliseconds. Default: 100 */
|
||||
pollMs?: number;
|
||||
/** Argument passed to the predicate function in the page context. */
|
||||
arg?: unknown;
|
||||
/** Custom error message prepended to the timeout error. */
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* pw_wait_predicate — polls an arbitrary predicate inside the page context
|
||||
* until it returns truthy or the timeout elapses.
|
||||
*
|
||||
* Thin wrapper around `page.waitForFunction` with friendlier defaults and
|
||||
* optional custom error messages. More flexible than `waitForSelector` for
|
||||
* computed conditions such as "card has class .border-red" or "exactly 1
|
||||
* element is highlighted".
|
||||
*/
|
||||
export async function pw_wait_predicate<T = unknown>(
|
||||
page: Page,
|
||||
predicate: string | ((arg?: unknown) => unknown),
|
||||
opts: PwWaitOptions = {},
|
||||
): Promise<T> {
|
||||
const timeoutMs = opts.timeoutMs ?? 5000;
|
||||
const pollMs = opts.pollMs ?? 100;
|
||||
|
||||
try {
|
||||
const handle = await page.waitForFunction(predicate, opts.arg, {
|
||||
timeout: timeoutMs,
|
||||
polling: pollMs,
|
||||
});
|
||||
return handle.jsonValue() as Promise<T>;
|
||||
} catch (err) {
|
||||
if (opts.message) {
|
||||
const cause = err instanceof Error ? err : new Error(String(err));
|
||||
throw new Error(`${opts.message}: ${cause.message}`, { cause });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "fn-registry-frontend-functions",
|
||||
"type": "module",
|
||||
"private": true
|
||||
}
|
||||
@@ -35,11 +35,13 @@
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"playwright": "^1.60.0",
|
||||
"postcss": "^8.5.8",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.3"
|
||||
"vite": "^8.0.3",
|
||||
"vitest": "^4.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+285
@@ -69,6 +69,9 @@ importers:
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))(tsx@4.21.0))
|
||||
playwright:
|
||||
specifier: ^1.60.0
|
||||
version: 1.60.0
|
||||
postcss:
|
||||
specifier: ^8.5.8
|
||||
version: 8.5.8
|
||||
@@ -87,6 +90,9 @@ importers:
|
||||
vite:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))(tsx@4.21.0)
|
||||
vitest:
|
||||
specifier: ^4.1.6
|
||||
version: 4.1.6(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))(tsx@4.21.0))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -283,6 +289,9 @@ packages:
|
||||
'@fontsource-variable/geist@5.2.8':
|
||||
resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@mantine/charts@9.0.0':
|
||||
resolution: {integrity: sha512-TnbjiT2tXZDAQWZrv/+Xu3JKYjPiTfO5jSIbcwnxZSVtLI+PIxA7zrSps+it/Nx3ch8GHpDizJ7UArC0UfmNkQ==}
|
||||
peerDependencies:
|
||||
@@ -471,6 +480,9 @@ packages:
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||
|
||||
'@types/d3-array@3.2.2':
|
||||
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
||||
|
||||
@@ -498,6 +510,12 @@ packages:
|
||||
'@types/d3-timer@3.0.2':
|
||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
|
||||
'@types/estree@1.0.9':
|
||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||
|
||||
'@types/react-dom@19.2.3':
|
||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||
peerDependencies:
|
||||
@@ -522,6 +540,39 @@ packages:
|
||||
babel-plugin-react-compiler:
|
||||
optional: true
|
||||
|
||||
'@vitest/expect@4.1.6':
|
||||
resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==}
|
||||
|
||||
'@vitest/mocker@4.1.6':
|
||||
resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@4.1.6':
|
||||
resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==}
|
||||
|
||||
'@vitest/runner@4.1.6':
|
||||
resolution: {integrity: sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==}
|
||||
|
||||
'@vitest/snapshot@4.1.6':
|
||||
resolution: {integrity: sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==}
|
||||
|
||||
'@vitest/spy@4.1.6':
|
||||
resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==}
|
||||
|
||||
'@vitest/utils@4.1.6':
|
||||
resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
attr-accept@2.2.5:
|
||||
resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -530,10 +581,17 @@ packages:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
chai@6.2.2:
|
||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -602,6 +660,9 @@ packages:
|
||||
dom-helpers@5.2.1:
|
||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||
|
||||
es-module-lexer@2.1.0:
|
||||
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
|
||||
|
||||
es-toolkit@1.45.1:
|
||||
resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==}
|
||||
|
||||
@@ -610,6 +671,9 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
|
||||
eventemitter3@5.0.4:
|
||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||
|
||||
@@ -617,6 +681,10 @@ packages:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
|
||||
expect-type@1.3.0:
|
||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -633,6 +701,11 @@ packages:
|
||||
resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -758,6 +831,9 @@ packages:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -767,6 +843,12 @@ packages:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
obug@2.1.1:
|
||||
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -774,6 +856,16 @@ packages:
|
||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
playwright-core@1.60.0:
|
||||
resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.60.0:
|
||||
resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
postcss-js@4.1.0:
|
||||
resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==}
|
||||
engines: {node: ^12 || ^14 || >= 16}
|
||||
@@ -919,6 +1011,9 @@ packages:
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
sigma@3.0.2:
|
||||
resolution: {integrity: sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==}
|
||||
|
||||
@@ -926,6 +1021,12 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
std-env@4.1.0:
|
||||
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
|
||||
|
||||
sugarss@5.0.1:
|
||||
resolution: {integrity: sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw==}
|
||||
engines: {node: '>=18.0'}
|
||||
@@ -942,10 +1043,21 @@ packages:
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
tinyexec@1.1.2:
|
||||
resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinyrainbow@3.1.0:
|
||||
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
@@ -1037,6 +1149,52 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest@4.1.6:
|
||||
resolution: {integrity: sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==}
|
||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@opentelemetry/api': ^1.9.0
|
||||
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||
'@vitest/browser-playwright': 4.1.6
|
||||
'@vitest/browser-preview': 4.1.6
|
||||
'@vitest/browser-webdriverio': 4.1.6
|
||||
'@vitest/coverage-istanbul': 4.1.6
|
||||
'@vitest/coverage-v8': 4.1.6
|
||||
'@vitest/ui': 4.1.6
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser-playwright':
|
||||
optional: true
|
||||
'@vitest/browser-preview':
|
||||
optional: true
|
||||
'@vitest/browser-webdriverio':
|
||||
optional: true
|
||||
'@vitest/coverage-istanbul':
|
||||
optional: true
|
||||
'@vitest/coverage-v8':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/runtime@7.29.2': {}
|
||||
@@ -1162,6 +1320,8 @@ snapshots:
|
||||
|
||||
'@fontsource-variable/geist@5.2.8': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@mantine/charts@9.0.0(@mantine/core@9.0.0(@mantine/hooks@9.0.0(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@9.0.0(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.4)(react@19.2.4)(redux@5.0.1))':
|
||||
dependencies:
|
||||
'@mantine/core': 9.0.0(@mantine/hooks@9.0.0(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -1315,6 +1475,11 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
assertion-error: 2.0.1
|
||||
|
||||
'@types/d3-array@3.2.2': {}
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
@@ -1339,6 +1504,10 @@ snapshots:
|
||||
|
||||
'@types/d3-timer@3.0.2': {}
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
'@types/estree@1.0.9': {}
|
||||
|
||||
'@types/react-dom@19.2.3(@types/react@19.2.14)':
|
||||
dependencies:
|
||||
'@types/react': 19.2.14
|
||||
@@ -1354,12 +1523,59 @@ snapshots:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.7
|
||||
vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))(tsx@4.21.0)
|
||||
|
||||
'@vitest/expect@4.1.6':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/spy': 4.1.6
|
||||
'@vitest/utils': 4.1.6
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.1.6(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))(tsx@4.21.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.6
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))(tsx@4.21.0)
|
||||
|
||||
'@vitest/pretty-format@4.1.6':
|
||||
dependencies:
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/runner@4.1.6':
|
||||
dependencies:
|
||||
'@vitest/utils': 4.1.6
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@4.1.6':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.1.6
|
||||
'@vitest/utils': 4.1.6
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@4.1.6': {}
|
||||
|
||||
'@vitest/utils@4.1.6':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.1.6
|
||||
convert-source-map: 2.0.0
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
attr-accept@2.2.5: {}
|
||||
|
||||
camelcase-css@2.0.1: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
@@ -1415,6 +1631,8 @@ snapshots:
|
||||
'@babel/runtime': 7.29.2
|
||||
csstype: 3.2.3
|
||||
|
||||
es-module-lexer@2.1.0: {}
|
||||
|
||||
es-toolkit@1.45.1: {}
|
||||
|
||||
esbuild@0.27.4:
|
||||
@@ -1446,10 +1664,16 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.27.4
|
||||
'@esbuild/win32-x64': 0.27.4
|
||||
|
||||
estree-walker@3.0.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
|
||||
eventemitter3@5.0.4: {}
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
expect-type@1.3.0: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.4):
|
||||
@@ -1460,6 +1684,9 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -1551,14 +1778,30 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
obug@2.1.1: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.4: {}
|
||||
|
||||
playwright-core@1.60.0: {}
|
||||
|
||||
playwright@1.60.0:
|
||||
dependencies:
|
||||
playwright-core: 1.60.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
postcss-js@4.1.0(postcss@8.5.8):
|
||||
dependencies:
|
||||
camelcase-css: 2.0.1
|
||||
@@ -1728,6 +1971,8 @@ snapshots:
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
sigma@3.0.2(graphology-types@0.24.8):
|
||||
dependencies:
|
||||
events: 3.3.0
|
||||
@@ -1737,6 +1982,10 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
std-env@4.1.0: {}
|
||||
|
||||
sugarss@5.0.1(postcss@8.5.8):
|
||||
dependencies:
|
||||
postcss: 8.5.8
|
||||
@@ -1747,11 +1996,17 @@ snapshots:
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@1.1.2: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
|
||||
tinyrainbow@3.1.0: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsx@4.21.0:
|
||||
@@ -1821,3 +2076,33 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
|
||||
vitest@4.1.6(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))(tsx@4.21.0)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.6
|
||||
'@vitest/mocker': 4.1.6(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))(tsx@4.21.0))
|
||||
'@vitest/pretty-format': 4.1.6
|
||||
'@vitest/runner': 4.1.6
|
||||
'@vitest/snapshot': 4.1.6
|
||||
'@vitest/spy': 4.1.6
|
||||
'@vitest/utils': 4.1.6
|
||||
es-module-lexer: 2.1.0
|
||||
expect-type: 1.3.0
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.1
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.4
|
||||
std-env: 4.1.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.1.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))(tsx@4.21.0)
|
||||
why-is-node-running: 2.3.0
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
Reference in New Issue
Block a user