| 1 | import { test, expect } from "@playwright/test"; |
1 | import { test, expect } from "@playwright/test"; |
| 2 | −import { TEST_EMAIL, TEST_PASSWORD, lastLinkFromLog, login, noConsoleErrors, readEnv, sql } from "./helpers"; |
2 | +import { TEST_EMAIL, TEST_PASSWORD, lastLinkFromLog, login, noConsoleErrors, readEnv, restoreSession, saveSession, sql } from "./helpers"; |
| 3 | |
3 | |
| 4 | /** |
4 | /** |
| 5 | * Full workflow against the dev server (EMAIL_DRY_RUN=1 EMAIL_DRY_RUN_PRINT=1 so links land in the log): |
5 | * Full workflow against the dev server (EMAIL_DRY_RUN=1 EMAIL_DRY_RUN_PRINT=1 so links land in the log): |
| 52 | |
52 | |
| 53 | test("add + validate provider keys (real providers)", async ({ page }) => { |
53 | test("add + validate provider keys (real providers)", async ({ page }) => { |
| 54 | await login(page, email, password); |
54 | await login(page, email, password); |
| |
55 | + await saveSession(page); |
| 55 | await page.goto("/app/settings/providers"); |
56 | await page.goto("/app/settings/providers"); |
| 56 | const names: Record<string, RegExp> = { anthropic: /^Anthropic$/, openai: /^OpenAI$/, xai: /^xAI$/, gemini: /^Google Gemini$/ }; |
57 | const names: Record<string, RegExp> = { anthropic: /^Anthropic$/, openai: /^OpenAI$/, xai: /^xAI$/, gemini: /^Google Gemini$/ }; |
| 57 | for (const [provider, envName] of [ |
58 | for (const [provider, envName] of [ |
| 80 | }); |
81 | }); |
| 81 | |
82 | |
| 82 | test("select a model, send a message and receive a real stream", async ({ page }) => { |
83 | test("select a model, send a message and receive a real stream", async ({ page }) => { |
| 83 | − await login(page, email, password); |
84 | + await restoreSession(page); |
| 84 | await page.goto("/app/chat"); |
85 | await page.goto("/app/chat"); |
| 85 | await page.getByRole("button", { name: /select model/i }).click(); |
86 | await page.getByRole("button", { name: /select model/i }).click(); |
| 86 | await page.getByLabel(/search models/i).fill("haiku 4.5"); |
87 | await page.getByLabel(/search models/i).fill("haiku 4.5"); |
| 98 | }); |
99 | }); |
| 99 | |
100 | |
| 100 | test("conversation persists after reload and a second model works", async ({ page }) => { |
101 | test("conversation persists after reload and a second model works", async ({ page }) => { |
| 101 | − await login(page, email, password); |
102 | + await restoreSession(page); |
| 102 | await page.goto(conversationUrl); |
103 | await page.goto(conversationUrl); |
| 103 | await expect(page.getByText(/PONG/).first()).toBeVisible(); |
104 | await expect(page.getByText(/PONG/).first()).toBeVisible(); |
| 104 | // switch to an OpenAI model in the same conversation and continue |
105 | // switch to an OpenAI model in the same conversation and continue |
| 114 | }); |
115 | }); |
| 115 | |
116 | |
| 116 | test("model configuration is capability-driven", async ({ page }) => { |
117 | test("model configuration is capability-driven", async ({ page }) => { |
| 117 | − await login(page, email, password); |
118 | + await restoreSession(page); |
| 118 | await page.goto("/app/chat"); |
119 | await page.goto("/app/chat"); |
| 119 | await page.getByRole("button", { name: /select model/i }).click(); |
120 | await page.getByRole("button", { name: /select model/i }).click(); |
| 120 | await page.getByLabel(/search models/i).fill("gpt-5.5"); |
121 | await page.getByLabel(/search models/i).fill("gpt-5.5"); |
| 124 | await expect(page.getByText(/^Effort$/)).toBeVisible(); |
125 | await expect(page.getByText(/^Effort$/)).toBeVisible(); |
| 125 | // seed/stop are not supported by the Responses API → never shown |
126 | // seed/stop are not supported by the Responses API → never shown |
| 126 | await expect(page.getByText(/^Seed$/)).toHaveCount(0); |
127 | await expect(page.getByText(/^Seed$/)).toHaveCount(0); |
| 127 | − await expect(page.getByText(/stop sequences/i)).toHaveCount(0); |
128 | + await expect(page.getByText(/^Stop sequences$/)).toHaveCount(0); |
| |
129 | + await expect(page.getByText(/not available for this model/i)).toContainText(/seed/i); |
| 128 | await page.keyboard.press("Escape"); |
130 | await page.keyboard.press("Escape"); |
| 129 | }); |
131 | }); |
| 130 | |
132 | |
| 131 | test("usage dashboard shows the requests", async ({ page }) => { |
133 | test("usage dashboard shows the requests", async ({ page }) => { |
| 132 | − await login(page, email, password); |
134 | + await restoreSession(page); |
| 133 | await page.goto("/app/usage"); |
135 | await page.goto("/app/usage"); |
| 134 | await expect(page.getByText(/requests/i).first()).toBeVisible(); |
136 | await expect(page.getByText(/requests/i).first()).toBeVisible(); |
| 135 | await expect.poll(() => Number(sql(`select count(*) from usage_records ur join users u on u.id=ur.user_id where u.email='${email}'`))).toBeGreaterThanOrEqual(2); |
137 | await expect.poll(() => Number(sql(`select count(*) from usage_records ur join users u on u.id=ur.user_id where u.email='${email}'`))).toBeGreaterThanOrEqual(2); |
| 136 | }); |
138 | }); |
| 137 | |
139 | |
| 138 | test("change password, logout, login again", async ({ page }) => { |
140 | test("change password, logout, login again", async ({ page }) => { |
| 139 | − await login(page, email, password); |
141 | + await restoreSession(page); |
| 140 | await page.goto("/app/settings/security"); |
142 | await page.goto("/app/settings/security"); |
| 141 | const newPassword = `${password}-2`; |
143 | const newPassword = `${password}-2`; |
| 142 | await page.getByLabel(/current password/i).fill(password); |
144 | await page.getByLabel(/current password/i).fill(password); |