import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { createClient } from "@libsql/client";
import { chromium, type FullConfig } from "@playwright/test";
/**
* Global setup: authenticate a test user (email/password; there is no dev auth
* bypass) and seed one design with a known fixture HTML so specs run against
* deterministic content. Writes:
* e2e/.auth/state.json - signed session storageState
* e2e/.auth/seed.json - { designId } of the seeded design
*/
export const E2E_EMAIL = "e2e@local.test";
export const E2E_PASSWORD = "password-e2e-1234";
export const SEED_TITLE = "E2E Seed Design";
const AUTH_DIR = process.env.E2E_AUTH_DIR
? path.resolve(process.env.E2E_AUTH_DIR)
: path.join(import.meta.dirname, ".auth");
const STATE_PATH = path.join(AUTH_DIR, "state.json");
const SEED_PATH = path.join(AUTH_DIR, "seed.json");
const BROWSER_CHANNEL = process.env.E2E_BROWSER_CHANNEL;
const E2E_DATABASE_URL =
process.env.E2E_DATABASE_URL ??
`file:${path.join(import.meta.dirname, "..", "data", "e2e.db")}`;
/**
* Fixture HTML with distinct, text-identifiable elements. Plain inline styles
* (no CDN) so the layout is deterministic and offline. The flex row of two
* buttons exercises reorder/move; headings and paragraphs exercise select.
*/
export const FIXTURE_HTML = `
E2E Fixture
E2E Hero Heading
First fixture paragraph for selection tests.
Second fixture paragraph for selection tests.
Alpha Button
Beta Button
Variant CTA
Token swatch sample
Fixture Card Title
Card body text inside a nested container.
`;
async function postAction(
request: import("@playwright/test").APIRequestContext,
baseURL: string,
name: string,
input: Record,
): Promise {
const res = await request.post(`${baseURL}/_agent-native/actions/${name}`, {
data: input,
headers: { "Content-Type": "application/json" },
});
if (!res.ok()) {
throw new Error(
`action ${name} failed: ${res.status()} ${await res.text()}`,
);
}
return res.json();
}
function componentIndexId(designId: string, name: string): string {
return `ci_${designId}_${name.toLowerCase().replace(/[^a-z0-9]/g, "_")}`;
}
export async function seedComponentVariantMetadata(
designId: string,
): Promise {
const client = createClient({ url: E2E_DATABASE_URL });
const name = "E2EButton";
const now = new Date().toISOString();
const variants = JSON.stringify({
variant: ["primary", "secondary", "ghost"],
size: ["sm", "md", "lg"],
});
const props = JSON.stringify([
{ name: "variant", type: "primary | secondary | ghost" },
{ name: "size", type: "sm | md | lg" },
]);
try {
const result = await client.execute({
sql: `
UPDATE component_index
SET variants = ?, props = ?, file_path = ?, export_name = ?, updated_at = ?
WHERE design_id = ? AND name = ?
`,
args: [variants, props, "index.html", name, now, designId, name],
});
if (result.rowsAffected > 0) return;
await client.execute({
sql: `
INSERT INTO component_index (
id, design_id, name, file_path, export_name, props, variants,
runtime_selectors, owner_email, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
args: [
componentIndexId(designId, name),
designId,
name,
"index.html",
name,
props,
variants,
JSON.stringify(['[data-agent-native-node-id="e2e-component-button"]']),
E2E_EMAIL,
now,
now,
],
});
} finally {
client.close();
}
}
export default async function globalSetup(config: FullConfig) {
const baseURL =
(config.projects[0]?.use?.baseURL as string | undefined) ??
"http://127.0.0.1:9333";
await mkdir(AUTH_DIR, { recursive: true });
const browser = await chromium.launch(
BROWSER_CHANNEL ? { channel: BROWSER_CHANNEL } : {},
);
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto(`${baseURL}/_agent-native/sign-in`, {
waitUntil: "domcontentloaded",
});
const isSignIn = async () => /sign in/i.test(await page.title());
if (await isSignIn()) {
// Try to create the account; if it already exists, fall back to sign in.
await page.locator("#s-email").fill(E2E_EMAIL);
await page.locator("#s-pass").fill(E2E_PASSWORD);
await page.locator("#s-pass2").fill(E2E_PASSWORD);
await page.locator("#signup-form button[type='submit']").click();
await page.waitForTimeout(2500);
if (await isSignIn()) {
// Account exists; switch to the Sign in tab and log in.
await page
.getByRole("button", { name: "Sign in", exact: true })
.first()
.click()
.catch(() => {});
await page.locator("#l-email").fill(E2E_EMAIL);
await page.locator("#l-pass").fill(E2E_PASSWORD);
await page.locator("#login-form button[type='submit']").click();
await page.waitForTimeout(2500);
}
}
await page
.waitForFunction(() => !/sign in/i.test(document.title), null, {
timeout: 20_000,
})
.catch(() => {});
await context.storageState({ path: STATE_PATH });
// Seed a design + fixture file via the authenticated action surface.
const created = await postAction(
context.request,
baseURL,
"create-design",
{
title: SEED_TITLE,
projectType: "prototype",
},
);
const designId: string =
created?.id ?? created?.data?.id ?? created?.design?.id;
if (!designId) {
throw new Error(
`create-design did not return an id: ${JSON.stringify(created)}`,
);
}
await postAction(context.request, baseURL, "create-file", {
designId,
filename: "index.html",
content: FIXTURE_HTML,
fileType: "html",
});
await postAction(context.request, baseURL, "index-components", {
designId,
});
await seedComponentVariantMetadata(designId);
await writeFile(SEED_PATH, JSON.stringify({ designId }, null, 2));
// eslint-disable-next-line no-console
console.log(`[e2e] seeded design ${designId} for ${E2E_EMAIL}`);
} finally {
await browser.close();
}
}