Files
openclaw/ui/src/e2e/model-setup.e2e.test.ts
T
2026-08-01 00:26:40 +08:00

890 lines
33 KiB
TypeScript

// Control UI tests cover guided model setup against a mocked Gateway.
import { mkdir } from "node:fs/promises";
import path from "node:path";
import { chromium, type Browser } from "playwright";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
canRunPlaywrightChromium,
installMockGateway,
resolvePlaywrightChromiumExecutablePath,
startControlUiE2eServer,
type ControlUiE2eServer,
} from "../test-helpers/control-ui-e2e.ts";
const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
const localPrepareOptions = [
{
id: "ollama",
brandId: "ollama",
label: "Ollama",
hint: "Connect to an Ollama server and select a cloud or local model",
actionLabel: "Choose connection",
},
{
id: "llama-cpp",
brandId: "llama-cpp",
label: "Local model (llama.cpp)",
hint: "Download and run a private GGUF model",
actionLabel: "Review download",
},
{
id: "lmstudio",
brandId: "lmstudio",
label: "LM Studio",
hint: "Connect to a running LM Studio server and use an already loaded model",
actionLabel: "Connect server",
icon: "https://cdn.simpleicons.org/lmstudio",
website: "https://lmstudio.ai/download",
},
];
let browser: Browser;
let server: ControlUiE2eServer;
describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => {
beforeAll(async () => {
if (!chromiumAvailable) {
throw new Error(`Playwright Chromium is unavailable at ${chromiumExecutablePath}`);
}
server = await startControlUiE2eServer();
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
});
afterAll(async () => {
await browser?.close();
await server?.close();
});
it("hands first-run model setup to the custodian in onboarding chrome", async () => {
const context = await browser.newContext({
locale: "en-US",
serviceWorkers: "block",
viewport: { height: 900, width: 1280 },
});
const page = await context.newPage();
const gateway = await installMockGateway(page, {
featureMethods: [
"chat.metadata",
"chat.startup",
"openclaw.setup.detect",
"openclaw.setup.activate",
"openclaw.chat",
],
methodResponses: {
"openclaw.setup.detect": {
candidates: [
{
kind: "codex-cli",
brandId: "openai",
label: "Codex CLI",
detail: "Signed in locally",
modelRef: "openai/gpt-5",
recommended: true,
credentials: true,
},
],
manualProviders: [{ id: "openai", label: "OpenAI" }],
workspace: "/tmp/openclaw-e2e",
setupComplete: false,
},
"openclaw.setup.activate": {
ok: true,
modelRef: "openai/gpt-5",
latencyMs: 73,
lines: ["Model ready"],
},
"openclaw.chat": {
sessionId: "e2e-custodian",
reply: "## Hi, I'm OpenClaw",
action: "none",
question: {
id: "onboarding-next-step",
header: "Next step",
question: "What would you like to do first?",
options: [
{ label: "Talk to my agent", reply: "talk to agent", recommended: true },
{ label: "Connect WhatsApp", reply: "connect whatsapp" },
],
isOther: true,
skipAction: "exit",
},
},
},
});
try {
const response = await page.goto(`${server.baseUrl}settings/model-setup?firstRun=1`);
expect(response?.status()).toBe(200);
await page.getByRole("heading", { name: "Connect a verified AI model" }).waitFor();
const candidate = page.locator('[data-candidate-kind="codex-cli"]');
await expect.poll(() => candidate.locator('[data-provider-icon="codex"]').count()).toBe(1);
await candidate.getByRole("button", { name: "Test & use" }).click();
const detect = await gateway.waitForRequest("openclaw.setup.detect");
expect(detect.params).toEqual({});
const activate = await gateway.waitForRequest("openclaw.setup.activate");
expect(activate.params).toEqual({ kind: "codex-cli", modelRef: "openai/gpt-5" });
await page.getByRole("heading", { name: "Connection verified" }).waitFor();
await expect
.poll(async () => page.locator(".model-setup-success").textContent())
.toContain("openai/gpt-5");
await expect
.poll(async () => page.locator(".model-setup-success").textContent())
.toContain("Verified in 73 ms");
await page.getByRole("button", { name: "Continue setup" }).click();
await expect.poll(() => new URL(page.url()).pathname).toBe("/custodian");
expect(new URL(page.url()).searchParams.get("onboarding")).toBe("1");
await page.getByRole("heading", { name: "OpenClaw", exact: true }).waitFor();
await expect
.poll(() => page.locator(".shell").getAttribute("class"))
.toContain("shell--onboarding");
expect(await page.locator(".shell-nav").isVisible()).toBe(false);
const chatRequest = await gateway.waitForRequest("openclaw.chat");
expect(chatRequest.params).toMatchObject({
sessionId: expect.stringMatching(/^control-ui-onboarding-/u),
welcomeVariant: "onboarding",
});
await page.getByRole("button", { name: "Skip for now" }).click();
await expect.poll(() => new URL(page.url()).pathname).toBe("/chat");
await expect
.poll(() => page.locator(".shell").getAttribute("class"))
.not.toContain("shell--onboarding");
const userTurns = (await gateway.getRequests("openclaw.chat")).filter((request) => {
const params = request.params;
return typeof params === "object" && params !== null && "message" in params;
});
expect(userTurns).toHaveLength(0);
} finally {
await context.close();
}
});
it("completes device-code sign-in and re-detects the configured model", async () => {
const context = await browser.newContext({
locale: "en-US",
serviceWorkers: "block",
viewport: { height: 900, width: 1280 },
});
const page = await context.newPage();
const initialDetection = {
candidates: [],
manualProviders: [],
authOptions: [
{
id: "provider-device-code",
label: "Provider account",
kind: "device-code",
featured: true,
},
],
workspace: "/tmp/openclaw-e2e",
setupComplete: false,
};
const gateway = await installMockGateway(page, {
featureMethods: [
"chat.metadata",
"chat.startup",
"openclaw.setup.detect",
"openclaw.setup.auth.start",
"wizard.next",
],
methodResponses: {
"openclaw.setup.detect": initialDetection,
"openclaw.setup.auth.start": {
sessionId: "device-code-session",
done: false,
status: "running",
},
"wizard.next": {
sequence: [
{
done: false,
status: "running",
step: {
id: "device-code",
type: "note",
title: "Authorize device",
externalUrl: "https://example.com/device",
deviceCode: { code: "ABCD-1234", expiresInMinutes: 14 },
},
},
{ done: true, status: "done" },
],
},
},
});
try {
const response = await page.goto(`${server.baseUrl}settings/model-setup`);
expect(response?.status()).toBe(200);
await page.getByRole("button", { name: "Pair" }).click();
const start = await gateway.waitForRequest("openclaw.setup.auth.start");
expect(start.params).toMatchObject({ authChoice: "provider-device-code" });
await page.getByText("ABCD-1234").waitFor();
await page.getByText("Expires in 14 minutes").waitFor();
const signInLink = page.getByRole("link", { name: "Open sign-in page" });
await expect.poll(() => signInLink.getAttribute("href")).toBe("https://example.com/device");
await gateway.setMethodResponse("openclaw.setup.detect", {
...initialDetection,
authOptions: [],
configuredModel: "provider/verified-model",
setupComplete: true,
});
const detectCountBeforeCompletion = (await gateway.getRequests("openclaw.setup.detect"))
.length;
await page.getByRole("button", { name: "Continue" }).click();
await expect.poll(async () => (await gateway.getRequests("wizard.next")).length).toBe(2);
const wizardRequests = await gateway.getRequests("wizard.next");
expect(wizardRequests[0]?.params).toMatchObject({ sessionId: expect.any(String) });
expect(wizardRequests[1]?.params).toMatchObject({
sessionId: expect.any(String),
answer: { stepId: "device-code" },
});
await expect
.poll(async () => (await gateway.getRequests("openclaw.setup.detect")).length)
.toBe(detectCountBeforeCompletion + 1);
await page.getByRole("heading", { name: "Connection verified" }).waitFor();
await expect
.poll(async () => page.locator(".model-setup-success").textContent())
.toContain("provider/verified-model");
} finally {
await context.close();
}
});
it("recovers Ollama setup in place after the local server starts", async () => {
const context = await browser.newContext({
colorScheme: "dark",
locale: "en-US",
serviceWorkers: "block",
viewport: { height: 900, width: 1280 },
});
const page = await context.newPage();
const initialDetection = {
candidates: [],
manualProviders: [],
prepareOptions: localPrepareOptions,
recommendedInstalls: [
{
id: "ollama",
brandId: "ollama",
label: "Ollama",
hint: "Run open models locally",
website: "https://ollama.com/download",
},
],
workspace: "/tmp/openclaw-e2e",
setupComplete: false,
};
const gateway = await installMockGateway(page, {
featureMethods: [
"chat.metadata",
"chat.startup",
"openclaw.setup.detect",
"openclaw.setup.activate",
"openclaw.setup.prepare.start",
"wizard.next",
],
methodResponses: {
"openclaw.setup.detect": initialDetection,
"openclaw.setup.prepare.start": {
sessionId: "ollama-prepare-session",
done: false,
status: "running",
},
"openclaw.setup.activate": {
ok: true,
modelRef: "ollama/qwen3:0.6b",
latencyMs: 284,
lines: ["Model ready"],
},
"wizard.next": {
sequence: [
{
done: false,
status: "running",
step: {
id: "ollama-mode",
type: "select",
message: "Ollama mode",
options: [
{
value: "cloud-local",
label: "Cloud + Local",
hint: "Route cloud and local models through your Ollama host",
},
{ value: "cloud-only", label: "Cloud only", hint: "Hosted Ollama models" },
{ value: "local-only", label: "Local only", hint: "Local models only" },
],
},
},
{
done: false,
status: "running",
step: {
id: "ollama-base-url",
type: "text",
message: "Ollama base URL",
initialValue: "http://127.0.0.1:11434",
},
},
{
done: false,
status: "running",
step: {
id: "ollama-retry",
type: "note",
title: "Ollama",
message: [
"Ollama could not be reached at http://127.0.0.1:11434.",
"Start or restart the Ollama server for this address.",
"If Ollama is not installed on that machine, download it at https://ollama.com/download",
"",
"Continue when it is running. OpenClaw will retry this address.",
].join("\n"),
},
},
{
done: false,
status: "running",
step: {
id: "ollama-retry-confirm",
type: "confirm",
message: "Retry this Ollama address now?",
initialValue: true,
},
},
{ done: true, status: "done" },
],
},
},
});
try {
const response = await page.goto(`${server.baseUrl}settings/model-setup`);
expect(response?.status()).toBe(200);
const localProviderIcons = page.locator(
[
'[data-prepare-choice="ollama"] [data-provider-icon="ollama"]',
'[data-prepare-choice="llama-cpp"] [data-provider-icon="llamacpp"]',
'[data-prepare-choice="lmstudio"] [data-provider-icon="lmstudio"]',
].join(","),
);
await expect.poll(() => localProviderIcons.count()).toBe(3);
const localProviderIconColors = await localProviderIcons.evaluateAll((icons) =>
icons.map((icon) => getComputedStyle(icon).color),
);
expect(localProviderIconColors).toHaveLength(3);
expect(new Set(localProviderIconColors).size).toBe(1);
await page
.locator('[data-prepare-choice="ollama"]')
.getByRole("button", { name: "Choose connection" })
.click();
const start = await gateway.waitForRequest("openclaw.setup.prepare.start");
expect(start.params).toMatchObject({ authChoice: "ollama" });
if (artifactDir) {
await mkdir(artifactDir, { recursive: true });
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "ollama-mode-select-desktop.png"),
});
}
await page.getByRole("radio", { name: /Local only/u }).check();
await page.getByRole("button", { name: "Continue" }).click();
const baseUrl = page.getByLabel("Ollama base URL");
await expect.poll(() => baseUrl.inputValue()).toBe("http://127.0.0.1:11434");
if (artifactDir) {
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "ollama-host-desktop.png"),
});
}
await page.getByRole("button", { name: "Submit" }).click();
await page.getByText("Ollama could not be reached at http://127.0.0.1:11434.").waitFor();
await page
.getByText("Continue when it is running. OpenClaw will retry this address.")
.waitFor();
if (artifactDir) {
await mkdir(artifactDir, { recursive: true });
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "ollama-recovery-desktop.png"),
});
}
await page.getByRole("button", { name: "Continue" }).click();
await page.getByText("Retry this Ollama address now?").waitFor();
await gateway.setMethodResponse("openclaw.setup.detect", {
...initialDetection,
candidates: [
{
kind: "provider-auto:ollama",
brandId: "ollama",
label: "Ollama",
detail: "qwen3:0.6b at http://127.0.0.1:11434",
modelRef: "ollama/qwen3:0.6b",
recommended: true,
credentials: true,
},
],
recommendedInstalls: [],
});
await page.getByRole("button", { name: "Yes" }).click();
await page.getByRole("heading", { name: "Connection verified" }).waitFor();
await expect
.poll(() => page.locator(".model-setup-success").textContent())
.toContain("ollama/qwen3:0.6b");
await expect
.poll(() => page.locator(".model-setup-success").textContent())
.toContain("Verified in 284 ms");
await expect
.poll(() => page.locator('.model-setup-success [data-provider-icon="ollama"]').count())
.toBe(1);
const activate = await gateway.waitForRequest("openclaw.setup.activate");
expect(activate.params).toEqual({
kind: "provider-auto:ollama",
modelRef: "ollama/qwen3:0.6b",
});
if (artifactDir) {
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "ollama-ready-desktop.png"),
});
}
await gateway.setMethodResponse("openclaw.setup.detect", {
...initialDetection,
candidates: [],
configuredModel: "ollama/qwen3:0.6b",
recommendedInstalls: [],
setupComplete: true,
});
await page.getByRole("button", { name: "Stay in settings" }).click();
const currentConnection = page.locator(".model-setup__current");
await currentConnection.getByText("ollama/qwen3:0.6b", { exact: true }).waitFor();
await expect
.poll(() => currentConnection.locator('[data-provider-icon="ollama"]').count())
.toBe(1);
if (artifactDir) {
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "ollama-main-desktop.png"),
});
}
const wizardRequests = await gateway.getRequests("wizard.next");
expect(wizardRequests).toHaveLength(5);
expect(wizardRequests[2]?.params).toMatchObject({
answer: {
stepId: "ollama-base-url",
value: "http://127.0.0.1:11434",
},
});
expect(wizardRequests[3]?.params).toMatchObject({
answer: { stepId: "ollama-retry" },
});
expect(wizardRequests[4]?.params).toMatchObject({
answer: { stepId: "ollama-retry-confirm", value: true },
});
} finally {
await context.close();
}
});
it("offers supported Google setup in an accessible provider picker", async () => {
const context = await browser.newContext({
colorScheme: "dark",
locale: "en-US",
serviceWorkers: "block",
viewport: { height: 1000, width: 1440 },
});
const page = await context.newPage();
const gateway = await installMockGateway(page, {
featureMethods: [
"chat.metadata",
"chat.startup",
"openclaw.setup.detect",
"openclaw.setup.activate",
"openclaw.setup.prepare.start",
],
methodResponses: {
"openclaw.setup.detect": {
candidates: [],
unavailableCandidates: [],
manualProviders: [
{
id: "qwen-cn",
brandId: "qwen",
groupLabel: "Qwen Cloud",
label: "Coding Plan API Key for China (subscription)",
hint: "Endpoint: coding.dashscope.aliyuncs.com",
},
{
id: "qwen-global",
brandId: "qwen",
groupLabel: "Qwen Cloud",
label: "Coding Plan API Key for Global/Intl (subscription)",
hint: "Endpoint: coding-intl.dashscope.aliyuncs.com",
},
{
id: "zai-cn",
brandId: "zai",
groupLabel: "Z.AI",
label: "Coding-Plan-CN",
},
{
id: "gemini-api-key",
brandId: "google",
groupLabel: "Google",
label: "Google AI Studio API key",
hint: "Supported API-key access from aistudio.google.com/apikey",
},
],
authOptions: [],
workspace: "/tmp/openclaw-e2e",
setupComplete: false,
},
"openclaw.setup.activate": {
ok: true,
modelRef: "qwen/qwen3-coder-plus",
latencyMs: 412,
lines: ["Model ready"],
},
},
});
try {
const response = await page.goto(`${server.baseUrl}settings/model-setup`);
expect(response?.status()).toBe(200);
await page.getByRole("heading", { name: "Connect a verified AI model" }).waitFor();
await expect.poll(() => page.getByText("Gemini CLI OAuth").count()).toBe(0);
await expect.poll(() => page.getByText("Found, but needs attention").count()).toBe(0);
const providerPicker = page.locator(".model-setup-provider-select");
const providerTrigger = providerPicker.locator(".model-setup-provider-select__trigger");
const manualProviderIsActive = (providerId: string) =>
page
.locator(`[data-manual-provider="${providerId}"]`)
.evaluate((element) => Reflect.get(element, "active") === true);
const manualProviderMenuReady = () =>
page
.locator("[data-manual-provider]")
.evaluateAll((options) =>
options.some((option) => Reflect.get(option, "active") === true),
);
const waitForProviderHide = () =>
providerPicker.evaluate(
(element) =>
new Promise<void>((resolve) => {
element.addEventListener("wa-after-hide", () => resolve(), { once: true });
}),
);
const providerIds = await page
.locator("[data-manual-provider]")
.evaluateAll((options) =>
options
.map((option) => (option instanceof HTMLElement ? option.dataset.manualProvider : null))
.filter((value): value is string => Boolean(value)),
);
const firstProviderId = providerIds[0]!;
const lastProviderId = providerIds.at(-1)!;
await providerTrigger.click();
await expect
.poll(() => providerPicker.evaluate((element) => element.hasAttribute("open")))
.toBe(true);
await expect
.poll(() => page.locator('[data-manual-provider="qwen-cn"]').getAttribute("aria-label"))
.toContain("Qwen Cloud");
await expect
.poll(() => page.locator('[data-manual-provider="zai-cn"]').getAttribute("aria-label"))
.toContain("Z.AI");
await expect
.poll(() =>
page.locator('[data-manual-provider="qwen-cn"] [data-provider-icon="alibaba"]').count(),
)
.toBe(1);
if (artifactDir) {
await mkdir(artifactDir, { recursive: true });
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "provider-picker-desktop.png"),
});
await page.setViewportSize({ height: 844, width: 390 });
await providerPicker.scrollIntoViewIfNeeded();
await page.screenshot({
animations: "disabled",
path: path.join(artifactDir, "provider-picker-mobile.png"),
});
await page.setViewportSize({ height: 1000, width: 1440 });
}
const providerHidden = waitForProviderHide();
await page.keyboard.press("Escape");
await providerHidden;
await expect
.poll(() => providerPicker.evaluate((element) => element.hasAttribute("open")))
.toBe(false);
const accessValue = page.locator('.model-setup__manual input[type="password"]');
await expect
.poll(() => providerTrigger.evaluate((element) => element === document.activeElement))
.toBe(true);
await page.keyboard.press("Enter");
await expect
.poll(() => providerPicker.evaluate((element) => element.hasAttribute("open")))
.toBe(true);
await expect.poll(manualProviderMenuReady).toBe(true);
await page.keyboard.press("Home");
await expect.poll(() => manualProviderIsActive(firstProviderId)).toBe(true);
await page.keyboard.press("End");
await expect.poll(() => manualProviderIsActive(lastProviderId)).toBe(true);
await page.keyboard.press("Home");
await expect.poll(() => manualProviderIsActive(firstProviderId)).toBe(true);
await page.keyboard.press("z");
await expect.poll(() => manualProviderIsActive("zai-cn")).toBe(true);
const zaiProviderHidden = waitForProviderHide();
await page.keyboard.press("Enter");
await zaiProviderHidden;
await expect.poll(() => providerTrigger.textContent()).toContain("Z.AI");
await expect
.poll(() => providerTrigger.evaluate((element) => element === document.activeElement))
.toBe(true);
await accessValue.fill("same-provider-secret");
await providerTrigger.click();
await expect.poll(manualProviderMenuReady).toBe(true);
const sameProviderHidden = waitForProviderHide();
await page.locator('[data-manual-provider="zai-cn"]').click();
await sameProviderHidden;
await expect.poll(() => accessValue.inputValue()).toBe("same-provider-secret");
await expect
.poll(() => providerTrigger.evaluate((element) => element === document.activeElement))
.toBe(true);
await providerTrigger.click();
await expect.poll(manualProviderMenuReady).toBe(true);
const providerHiddenBackward = waitForProviderHide();
await page.keyboard.press("Shift+Tab");
await providerHiddenBackward;
await expect
.poll(() => providerTrigger.evaluate((element) => element === document.activeElement))
.toBe(true);
await providerTrigger.click();
await expect.poll(manualProviderMenuReady).toBe(true);
await page.keyboard.press("Tab");
await expect
.poll(() => providerPicker.evaluate((element) => element.hasAttribute("open")))
.toBe(false);
await expect
.poll(() => accessValue.evaluate((element) => element === document.activeElement))
.toBe(true);
await accessValue.fill("sk-old-provider-secret");
await providerTrigger.click();
await expect.poll(manualProviderMenuReady).toBe(true);
const googleProviderHidden = waitForProviderHide();
await page.locator('[data-manual-provider="gemini-api-key"]').click();
await googleProviderHidden;
await expect.poll(() => providerTrigger.textContent()).toContain("Google");
await expect.poll(() => providerTrigger.textContent()).toContain("AI Studio API key");
await expect.poll(() => accessValue.inputValue()).toBe("");
await expect
.poll(() => providerTrigger.evaluate((element) => element === document.activeElement))
.toBe(true);
await providerTrigger.click();
const qwenProviderHidden = waitForProviderHide();
await page.locator('[data-manual-provider="qwen-cn"]').click();
await qwenProviderHidden;
await expect
.poll(() => providerPicker.evaluate((element) => element.hasAttribute("open")))
.toBe(false);
await expect.poll(() => providerTrigger.textContent()).toContain("Qwen Cloud");
await accessValue.fill("qwen-test-secret");
await expect.poll(() => accessValue.inputValue()).toBe("qwen-test-secret");
await page.evaluate(
() =>
new Promise<void>((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
}),
);
await page.getByRole("button", { name: "Connect & verify" }).click();
const activate = await gateway.waitForRequest("openclaw.setup.activate");
expect(activate.params).toEqual({
kind: "api-key",
authChoice: "qwen-cn",
apiKey: "qwen-test-secret",
});
await page.getByRole("heading", { name: "Connection verified" }).waitFor();
await page.getByText("qwen/qwen3-coder-plus", { exact: true }).waitFor();
if (artifactDir) {
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "success-desktop.png"),
});
await page.setViewportSize({ height: 844, width: 390 });
await expect
.poll(() =>
page.locator("openclaw-modal-dialog.nav-drawer").evaluate((element) => {
const dialog = element.shadowRoot
?.querySelector("wa-dialog")
?.shadowRoot?.querySelector("dialog");
return dialog?.open ?? false;
}),
)
.toBe(false);
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "success-mobile.png"),
});
await page.setViewportSize({ height: 1000, width: 1440 });
}
await expect
.poll(() => page.locator(".model-setup-success").textContent())
.toContain("Verified in 412 ms");
const detectCountBeforeDismiss = (await gateway.getRequests("openclaw.setup.detect")).length;
await page.getByRole("button", { name: "Stay in settings" }).click();
await expect
.poll(async () => (await gateway.getRequests("openclaw.setup.detect")).length)
.toBe(detectCountBeforeDismiss + 1);
await providerTrigger.click();
await expect.poll(manualProviderMenuReady).toBe(true);
const googleProviderHiddenAfterDismiss = waitForProviderHide();
await page.locator('[data-manual-provider="gemini-api-key"]').click();
await googleProviderHiddenAfterDismiss;
await expect.poll(() => providerTrigger.textContent()).toContain("Google");
await expect.poll(() => page.getByText("Gemini CLI OAuth").count()).toBe(0);
} finally {
await context.close();
}
});
it("verifies the current model connection", async () => {
const context = await browser.newContext({
locale: "en-US",
...(artifactDir
? { recordVideo: { dir: artifactDir, size: { height: 900, width: 1280 } } }
: {}),
serviceWorkers: "block",
viewport: { height: 900, width: 1280 },
});
const page = await context.newPage();
const gateway = await installMockGateway(page, {
featureMethods: [
"chat.metadata",
"chat.startup",
"openclaw.setup.detect",
"openclaw.setup.verify",
],
methodResponses: {
"openclaw.setup.detect": {
candidates: [
{
kind: "existing-model",
brandId: "openai",
label: "Current model",
detail: "openai/gpt-5 — already configured",
modelRef: "openai/gpt-5",
recommended: false,
credentials: true,
},
{
kind: "claude-cli",
brandId: "claude",
label: "Claude Code",
detail: "logged in",
modelRef: "claude-cli/claude-opus-5",
recommended: false,
credentials: true,
},
],
manualProviders: [],
workspace: "/tmp/openclaw-e2e",
configuredModel: "openai/gpt-5",
setupComplete: true,
},
"openclaw.setup.verify": {
ok: true,
modelRef: "openai/gpt-5",
latencyMs: 1234,
},
},
});
try {
const response = await page.goto(`${server.baseUrl}settings/model-setup`);
expect(response?.status()).toBe(200);
await expect
.poll(() => page.locator('[data-candidate-kind="existing-model"]').count())
.toBe(0);
await expect.poll(() => page.locator('[data-candidate-kind="claude-cli"]').count()).toBe(1);
if (artifactDir) {
await mkdir(artifactDir, { recursive: true });
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "configured-route-dedup-desktop.png"),
});
await page.setViewportSize({ height: 844, width: 390 });
await expect
.poll(() =>
page.locator("openclaw-modal-dialog.nav-drawer").evaluate((element) => {
const dialog = element.shadowRoot
?.querySelector("wa-dialog")
?.shadowRoot?.querySelector("dialog");
return dialog?.open ?? false;
}),
)
.toBe(false);
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "configured-route-dedup-mobile.png"),
});
await page.setViewportSize({ height: 900, width: 1280 });
}
await page.getByRole("button", { name: "Verify connection" }).click();
const verify = await gateway.waitForRequest("openclaw.setup.verify");
expect(verify.params).toEqual({});
await page.getByText("Answered in 1234 ms").waitFor();
const detectCountBeforeRefresh = (await gateway.getRequests("openclaw.setup.detect")).length;
await page.getByRole("button", { name: "Check again" }).click();
await expect
.poll(async () => (await gateway.getRequests("openclaw.setup.detect")).length)
.toBe(detectCountBeforeRefresh + 1);
await page.getByRole("button", { name: "Verify connection" }).waitFor();
expect(await page.getByText("Answered in 1234 ms").count()).toBe(0);
} finally {
await context.close();
}
});
});