Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 0 additions & 164 deletions e2e-install/10-happy-path.spec.ts

This file was deleted.

196 changes: 196 additions & 0 deletions e2e-install/10-setup-wizard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* Full initial-setup wizard walkthrough.
*
* Drives the real SetupWizard React component through every step in a live
* browser session:
* WiFi → (Update → auto-skip) → Credentials → AI Models → Local AI →
* Telegram → Completion overlay → desktop.
*
* This replaces the earlier API-driven happy-path smoke test. It covers:
* - Every step's UI renders + transitions correctly
* - /setup-api/* routes under each step respond as the UI expects
* - Middleware flips setup-complete users off /setup onto /
* - Session cookie issued by /setup-api/setup/complete lands us on
* the desktop shell without a separate login round-trip
*
* Runs first so the container starts with a clean setup state. Every
* downstream spec (settings/files/terminal/webapps/app-store/browser/
* chat/upgrade/power) assumes setup is already complete by the time
* it runs, which is exactly the state this test leaves behind.
*/
import fs from "node:fs";
import path from "node:path";
import { test, expect } from "@playwright/test";
import {
dockerExec,
readInstallLog,
waitForHttpReady,
} from "./helpers/container";
import { getStatus } from "./helpers/setup-api";

const env = loadEnvTest();

test.describe.configure({ mode: "serial" });

test.describe("fresh-install setup wizard (UI)", () => {
test("install.sh finished cleanly", async () => {
const log = await readInstallLog(20);
expect(log).toMatch(/ClawBox Setup Complete|Rebuilding|services started/i);
await expect(
dockerExec(["test", "!", "-f", "/home/clawbox/clawbox/.needs-install"]),
).resolves.toBeDefined();
});

test("setup status starts empty", async () => {
const status = await getStatus();
expect(status.setup_complete).toBe(false);
expect(status.wifi_configured).toBe(false);
expect(status.password_configured).toBe(false);
});

// One big browser-driven walk. Per-step tests would need shared
// session/storage state across tests, which Playwright doesn't do by
// default. A single test keeps the flow readable and lets the error
// point straight at whichever step broke.
test("walk through wizard end-to-end", async ({ page }) => {
test.setTimeout(5 * 60_000);
await waitForHttpReady(60_000);
await page.goto("/setup");

// ── Step 1: WiFi ─────────────────────────────────────────────
await expect(page.getByTestId("setup-step-wifi")).toBeVisible({ timeout: 30_000 });

// Dismiss the Ethernet shortcut — we want to exercise the real WiFi
// flow (scan → pick SSID → password → connect) since that's the path
// end users hit on first boot. The Ethernet button is listed first,
// so target the WiFi button by its translated label.
const connectWifiBtn = page.getByRole("button", { name: /Connect to WiFi/i });
await expect(connectWifiBtn).toBeVisible();
await connectWifiBtn.click();

// The fixture network list comes from scanWifiLive() under
// CLAWBOX_TEST_MODE=1 in src/lib/network.ts.
const wifiNetwork = page.getByRole("button", { name: "TestNet-Home" });
await expect(wifiNetwork).toBeVisible({ timeout: 15_000 });
await wifiNetwork.click();

await page.locator("#wifi-password").fill("wireless-pass");
await page.getByRole("button", { name: /^Connect$/ }).click();

// ── Step 2: Update (frequently auto-advances) ─────────────────
const updateStep = page.getByTestId("setup-step-update");
const credentialsStep = page.getByTestId("setup-step-credentials");

// UpdateStep auto-advances to credentials when update_completed is
// already set (which happens as soon as we hit /setup-api/update/status
// once). Race the two — whichever shows first wins.
const credVisibleFast = await credentialsStep
.waitFor({ state: "visible", timeout: 8_000 })
.then(() => true)
.catch(() => false);
if (!credVisibleFast) {
await expect(updateStep).toBeVisible({ timeout: 10_000 });
const continueBtn = updateStep.getByRole("button", { name: /Continue/i });
await continueBtn.click();
}
Comment thread
yalexx marked this conversation as resolved.
Resolved

// ── Step 3: Credentials ──────────────────────────────────────
await expect(credentialsStep).toBeVisible({ timeout: 30_000 });
await page.locator("#cred-password").fill("clawbox-e2e-pass");
await page.locator("#cred-confirm").fill("clawbox-e2e-pass");

// Hotspot is a switch — turn it OFF for this test so the subsequent
// fields aren't required. 20-settings later exercises the hotspot
// config path explicitly, so we're not losing coverage here.
const hotspotSwitch = page.getByRole("switch", { name: /Enable hotspot/i });
await expect(hotspotSwitch).toBeVisible({ timeout: 10_000 });
const hotspotChecked = await hotspotSwitch.getAttribute("aria-checked");
if (hotspotChecked === "true") {
await hotspotSwitch.click();
await expect(hotspotSwitch).toHaveAttribute("aria-checked", "false");
}
await page.getByRole("button", { name: /^Connect$/ }).click();

// ── Step 4: Primary AI Models ────────────────────────────────
// Credentials step Connects by posting to
// /setup-api/system/credentials (which spawns the chpasswd systemd
// unit; cold start is slow) + /setup-api/system/hotspot. 60s covers
// both on a container whose chpasswd service hasn't been hit before.
await expect(page.getByTestId("setup-step-ai-models")).toBeVisible({ timeout: 90_000 });
// Pick "OpenAI GPT" with a placeholder key: the configure route saves
// the profile without validating the key. 80-chat's beforeAll swaps
// in the real CLAWBOX_AI_API_KEY later, so this placeholder only has
// to flip ai_model_configured for the wizard to advance. The ClawBox
// AI tile opens an owner-portal modal rather than accepting a raw
// token, so it's not usable for fully-automated wizard flow.
await page.getByText("OpenAI GPT").click();
// OpenAI defaults to the "Subscription" tab (ChatGPT Plus / Pro OAuth).
// We need the "API Key" tab for the plain-token path.
await page.getByRole("button", { name: /^API Key$/ }).click();
await expect(page.locator("#ai-api-key")).toBeVisible({ timeout: 5_000 });
await page.locator("#ai-api-key").fill("sk-e2e-placeholder-key");
await page.getByRole("button", { name: /Connect to OpenAI GPT/i }).click();

// ── Step 5: Local AI ─────────────────────────────────────────
// AIModelsStep shows a multi-phase "Setting up OpenAI GPT" overlay
// between step 4 submission and step 5 render (phases animate at
// 0/2/5/12/22s + a readiness poll on the gateway). Budget 2min so
// the overlay has plenty of time to complete even on a slow runner.
await expect(page.getByTestId("setup-step-local-ai")).toBeVisible({ timeout: 120_000 });
// llama-server / ollama aren't installed in test mode, so clicking
// "Enable Gemma 4" would fail. Skip to the next step — local AI is
// optional in production too.
await page.getByRole("button", { name: /^Skip$/ }).click();

// ── Step 6: Telegram ─────────────────────────────────────────
const telegramStep = page.getByTestId("setup-step-telegram");
await expect(telegramStep).toBeVisible({ timeout: 30_000 });
if (env.TELEGRAM_BOT_TOKEN) {
await page.getByRole("textbox", { name: /Bot Token/i })
.fill(env.TELEGRAM_BOT_TOKEN);
await page.getByRole("button", { name: /Connect|Save/i }).click();
} else {
await page.getByRole("button", { name: /Skip for now/i }).click();
}

// ── Step 7: Completion overlay → desktop ─────────────────────
// The wizard's final step posts to /setup-api/setup/complete which
// sets the session cookie + redirects to `/`. The completion overlay
// is a transient intermediate state — may flash by too quickly to
// assert on — so we just wait for the URL to land on `/`.
await page.waitForURL("/", { timeout: 2 * 60_000 });

// Confirm the shelf launcher renders on the desktop. ChromeShelf
// includes a mobile-only + desktop-only variant with tailwind
// responsive classes; filter to the visible one.
await expect(
page.locator('[data-testid="shelf-launcher-button"]').filter({ visible: true }),
).toBeVisible({ timeout: 30_000 });
});

test("setup is complete after the wizard", async () => {
const status = await getStatus();
expect(status.setup_complete).toBe(true);
expect(status.wifi_configured).toBe(true);
expect(status.password_configured).toBe(true);
expect(status.ai_model_configured).toBe(true);
});
});

// ── Env loader (shared with other specs; kept local to avoid a helper
// import cycle when specs evolve independently) ─────────────────────

function loadEnvTest(): Record<string, string | undefined> {
const envPath = path.resolve(__dirname, ".env.test");
if (!fs.existsSync(envPath)) return {};
const raw = fs.readFileSync(envPath, "utf-8");
const out: Record<string, string> = {};
for (const line of raw.split("\n")) {
const m = line.match(/^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
if (!m) continue;
const val = m[2].replace(/^["']|["']$/g, "");
if (val) out[m[1]] = val;
}
return out;
}

9 changes: 9 additions & 0 deletions e2e-install/80-chat.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ test.describe("chat round trip", () => {
{ name: "clawbox_session", value: match![1], domain: "localhost", path: "/" },
]);

// The desktop opens the chat panel based on the ui_chat_open pref.
// Fresh-setup state leaves it closed; force it open so we don't have
// to hunt for the mascot-click sequence that toggles it.
await fetch(`${BASE_URL}/setup-api/preferences`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ui_chat_open: 1, ui_mascot_hidden: 1 }),
});
Comment thread
yalexx marked this conversation as resolved.
Resolved

await page.goto("/");

// ChatPopup auto-opens on the desktop shell — no launcher click needed.
Expand Down
Loading