Files
next-ai-draw-io/tests/e2e/settings-providers.spec.ts
T
dayuan.jiang 75ffc32a49 fix(providers): apply the review of PR #988
Five reviewers (Fable 5.1, Opus 5.5, Sonnet 5.5, Codex GPT-6 Astra, Codex
GPT-6.1 Sol) found no behavior or security regression; this commit applies
what they asked for.

Settings dialog: the automatic model list fetch only fills the list, which
waits behind 'Browse models'; it no longer opens the picker (the modal picker
took the focus from whatever the user was typing) and shows no error of its
own. One list request per provider at a time, so a click during the automatic
fetch sends nothing twice; the baseline that detects freshly entered
credentials is taken on every visit of a provider page; SGLang's prefilled
local example URL triggers no fetch. Entries saved with Azure's old
placeholder URL still count as having no URL. The base URL field of Azure and
of the custom preset is labelled required (new dictionary key baseUrlRequired).

Smaller code: the PROVIDER_INFO alias is gone (three files and the tests
import PROVIDERS), as are three logo values no code read, two unused type
re-exports, three unused exports and SINGLE_SYSTEM_PROVIDERS (the chat route
reads the record). Renamed for clarity: REQUIRED_KEY_VARS, requiredKeyEnv,
configuredUrl, LocalizedText. Seven stale comments rewritten; the custom
preset's note shortened.

Tests: the matrix gains six override shapes (a Vertex key alone, AWS keys with
a session token, headers with a URL, a trusted URL with a key, a key with a
server model's URL variable, '/' alone), 5843 rows; the new rows were
regenerated on the pre-refactor code and match it. The matrix and the server
credential tests clear every variable they read; the admin variable table
covers every provider; a test that derived its expectation from the function
under test is gone; the singleSystem, listNeedsNoKey, sdkKnowsEndpoint and
auto-detection sets are spelled out.

Docs: the READMEs say 25 providers and name the OpenAI-compatible preset; the
AI_PROVIDER lists are complete; the Azure example URL has its /openai suffix;
'Adding a Provider' names the tests to update.
2026-10-12 21:17:51 +09:00

737 lines
27 KiB
TypeScript

// The models tab of the settings: providers, their keys and models
import type { Locator } from "@playwright/test"
import {
backToModels,
expect,
openApp,
openSettings,
openSettingsTab,
savedModelConfigs,
sendMessage,
settingsDialog,
TWO_PROVIDERS,
test,
} from "./lib/fixtures"
// Three providers: one with a failed model, one not set up, one working
const PROVIDERS = {
version: 1,
selectedModelId: "m4",
providers: [
{
id: "p1",
provider: "openai",
apiKey: "sk-test",
models: [
{ id: "m1", modelId: "gpt-6-astra", validated: true },
{
id: "m2",
modelId: "gpt-6.1-sol",
validated: false,
validationError: "Model not found",
},
{ id: "m3", modelId: "gpt-6-luna" },
],
},
{ id: "p2", provider: "bedrock", apiKey: "", models: [] },
{
id: "p3",
provider: "deepseek",
apiKey: "sk-ds",
models: [{ id: "m4", modelId: "deepseek-chat", validated: true }],
},
],
}
test("a first provider: pick it, fill in the key, test, use it", async ({
page,
}) => {
const tested: string[] = []
await page.route("**/api/validate-model", async (route) => {
tested.push(route.request().postDataJSON().modelId)
await route.fulfill({ json: { valid: true, responseTime: 1400 } })
})
const listedWith: string[] = []
await page.route("**/api/provider-models", async (route) => {
listedWith.push(route.request().postDataJSON().apiKey)
await route.fulfill({
json: {
models: [{ id: "deepseek-v4-pro" }, { id: "deepseek-chat" }],
},
})
})
await openApp(page)
await openSettings(page)
const dialog = settingsDialog(page)
await expect(dialog.getByText("Use your own API key")).toBeVisible()
await dialog.getByTestId("pick-provider-deepseek").click()
// No model yet (DeepSeek lists its own), and the key field has the cursor
await expect(
dialog.getByRole("heading", { name: "DeepSeek", exact: true }),
).toBeVisible()
await expect(dialog.locator("#api-key")).toBeFocused()
await expect(dialog.getByTestId("step-1-todo")).toBeVisible()
await expect(dialog.getByTestId("step-2-todo")).toBeVisible()
const testButton = dialog.getByTestId("test-models")
await expect(testButton).toBeDisabled()
await expect(dialog.getByText("Fill in the API key first.")).toBeVisible()
// The key is in: the list is fetched once by itself, without taking the
// focus; it is behind "Browse models"
await dialog.locator("#api-key").fill("sk-1234")
await expect(dialog.getByTestId("step-1-done")).toBeVisible()
const browse = dialog.getByRole("button", { name: "Browse models" })
await expect(browse).toBeVisible()
await expect(dialog.locator("#api-key")).toBeFocused()
expect(listedWith).toEqual(["sk-1234"])
await browse.click()
await page.getByRole("option", { name: "deepseek-v4-pro" }).click()
expect(listedWith).toEqual(["sk-1234"])
await expect(dialog.locator('input[title="deepseek-v4-pro"]')).toBeVisible()
await expect(dialog.getByTestId("step-2-done")).toBeVisible()
await expect(testButton).toHaveText("Test deepseek-v4-pro")
await testButton.click()
// The visible panel, not the screen reader's copy
await expect(
dialog
.getByText("deepseek-v4-pro works")
.and(dialog.locator(':not([role="status"])')),
).toBeVisible()
await expect(dialog.getByTestId("step-3-done")).toBeVisible()
expect(tested).toEqual(["deepseek-v4-pro"])
// The next step: use it in the chat
await dialog
.getByRole("button", { name: "Use deepseek-v4-pro in the chat" })
.click()
await expect(dialog).toHaveCount(0)
await expect(page.getByTestId("model-selector")).toContainText(
"deepseek-v4-pro",
)
const config = await savedModelConfigs(page)
expect(config.selectedModelId).toBe(config.providers[0].models[0].id)
})
test("the provider picker searches, marks added ones, and Enter picks", async ({
page,
}) => {
await openApp(page, { config: PROVIDERS })
await page.getByTestId("model-selector").click()
// The model picker opens the settings straight on the provider picker
await page.getByText("Add a provider…").click()
const dialog = settingsDialog(page)
const search = dialog.getByRole("searchbox")
await expect(search).toBeFocused()
await expect(dialog.getByTestId("pick-provider-openai")).toContainText(
"Added",
)
await expect(dialog.getByTestId("pick-provider-bedrock")).toContainText(
"AWS credentials · Added",
)
await expect(
dialog.getByTestId("pick-provider-anthropic"),
).not.toContainText("Added")
await expect(dialog.locator('[data-testid^="pick-provider-"]')).toHaveCount(
25,
)
await search.fill("zzz")
await expect(dialog.getByText("No provider matches.")).toBeVisible()
await search.fill("kimi")
await expect(dialog.locator('[data-testid^="pick-provider-"]')).toHaveCount(
1,
)
await search.press("Enter")
await expect(
dialog.getByRole("heading", { name: "Kimi (Moonshot)", exact: true }),
).toBeVisible()
expect((await savedModelConfigs(page)).providers.at(-1).provider).toBe(
"kimi",
)
})
test("the provider list shows what needs attention", async ({ page }) => {
await openApp(page, { config: PROVIDERS })
await openSettings(page)
const dialog = settingsDialog(page)
await expect(dialog.getByTestId("provider-row-p1")).toContainText(
"1 failed",
)
await expect(dialog.getByTestId("provider-row-p2")).toContainText(
"Not set up yet",
)
await expect(dialog.getByTestId("provider-row-p3")).toContainText(
"1 working",
)
// The selected model's provider is the one in use
await expect(dialog.getByTestId("provider-row-p3")).toContainText("In use")
await expect(dialog.getByTestId("provider-row-p1")).not.toContainText(
"In use",
)
})
test("the model picker lists untested and failed models", async ({ page }) => {
await openApp(page, { config: PROVIDERS })
await page.getByTestId("model-selector").click()
// By the model config's id: the server may offer a model of that name
const option = (id: string) =>
page.locator(`[role="option"][data-value="${id}"]`)
await expect(option("m3")).toContainText("Untested")
await expect(option("m2")).toContainText("Failed")
await expect(option("m1")).not.toContainText(/Untested|Failed/)
// An untested model can be picked
await option("m3").click()
await expect(page.getByTestId("model-selector")).toContainText("gpt-6-luna")
})
test("an old setting to hide untested models is ignored", async ({ page }) => {
await openApp(page, {
config: { ...PROVIDERS, showUnvalidatedModels: false },
})
await page.getByTestId("model-selector").click()
await expect(page.locator('[role="option"][data-value="m3"]')).toBeVisible()
})
test("a refused key opens the settings on that provider", async ({ page }) => {
await page.route("**/api/chat", (route) =>
route.fulfill({
status: 401,
contentType: "application/json",
body: JSON.stringify({
type: "provider",
code: "invalid_api_key",
message: "Incorrect API key",
}),
}),
)
await openApp(page, { config: { ...PROVIDERS, selectedModelId: "m1" } })
await sendMessage(page, "Draw a box")
await page
.getByRole("button", { name: "Open model settings" })
.click({ timeout: 15000 })
const dialog = settingsDialog(page)
await expect(
dialog.getByRole("heading", { name: "OpenAI", exact: true }),
).toBeVisible()
// Back to the list from there
await dialog.locator("header button", { hasText: "Models" }).click()
await expect(dialog.getByTestId("provider-row-p1")).toBeVisible()
})
test("an access code error opens the general tab with the code field", async ({
page,
}) => {
await page.route("**/api/config", (route) =>
route.fulfill({ json: { accessCodeRequired: true } }),
)
await page.route("**/api/chat", (route) =>
route.fulfill({
status: 401,
contentType: "application/json",
body: JSON.stringify({
error: "Invalid or missing access code. Please configure it in Settings.",
}),
}),
)
await openApp(page)
await sendMessage(page, "Draw a box")
const dialog = settingsDialog(page)
await expect(dialog.locator("#access-code")).toBeVisible({ timeout: 15000 })
await expect(dialog.getByTestId("settings-tab-general")).toHaveAttribute(
"aria-current",
"page",
)
})
test("a refused key marks the key field until it changes", async ({ page }) => {
await page.route("**/api/validate-model", (route) =>
route.fulfill({
json: {
valid: false,
code: "invalid_api_key",
error: "Incorrect API key",
},
}),
)
await openApp(page, { config: PROVIDERS })
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-p3").click()
await dialog.getByTestId("test-models").click()
const key = dialog.locator("#api-key")
await expect(key).toHaveAttribute("aria-invalid", "true")
await expect(
dialog.getByText(
"The provider rejected this API key. Check that the key above was copied in full. Incorrect API key",
),
).toBeVisible()
// The visible line, not the screen reader's copy
await expect(
dialog
.getByText("1 model failed the test")
.and(dialog.locator(':not([role="status"])')),
).toBeVisible()
await key.fill("sk-new")
await expect(key).not.toHaveAttribute("aria-invalid", "true")
})
test("Use in the chat waits while an answer runs", async ({ page }) => {
// The chat request stays open until released
let release!: () => void
const released = new Promise<void>((r) => {
release = r
})
await page.route("**/api/chat", async (route) => {
await released
await route.fulfill({
status: 200,
contentType: "text/event-stream",
body: 'data: {"type":"start"}\n\ndata: {"type":"finish"}\n\ndata: [DONE]\n\n',
})
})
await page.route("**/api/validate-model", (route) =>
route.fulfill({ json: { valid: true, responseTime: 900 } }),
)
await openApp(page, { config: PROVIDERS })
await sendMessage(page, "Draw a box")
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-p3").click()
await dialog.getByTestId("test-models").click()
const use = dialog.getByRole("button", {
name: "Use deepseek-chat in the chat",
})
await expect(use).toBeDisabled()
release()
await expect(use).toBeEnabled({ timeout: 15000 })
})
test("more options open by themselves only when they matter", async ({
page,
}) => {
await openApp(page, {
config: {
version: 1,
providers: [
{ id: "a1", provider: "azure", apiKey: "k", models: [] },
{ id: "o1", provider: "openai", apiKey: "k", models: [] },
{
id: "o2",
provider: "openai",
apiKey: "k",
baseUrl: "https://proxy.example.com/v1",
models: [],
},
],
},
})
await openSettings(page)
const dialog = settingsDialog(page)
const back = () =>
dialog.locator("header button", { hasText: "Models" }).click()
// Azure's default URL is only an example
await dialog.getByTestId("provider-row-a1").click()
await expect(dialog.locator("#base-url")).toBeVisible()
await back()
await dialog.getByTestId("provider-row-o1").click()
await expect(dialog.locator("#base-url")).toHaveCount(0)
await back()
await dialog.getByTestId("provider-row-o2").click()
await expect(dialog.locator("#base-url")).toHaveValue(
"https://proxy.example.com/v1",
)
})
test("the models tab keeps its page across tabs, not across openings", async ({
page,
}) => {
await openApp(page, { config: PROVIDERS })
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-p1").click()
await dialog.getByTestId("settings-tab-drawing").click()
await expect(dialog.locator("#max-output-tokens")).toBeVisible()
await dialog.getByTestId("settings-tab-models").click()
await expect(
dialog.getByRole("heading", { name: "OpenAI", exact: true }),
).toBeVisible()
await page.keyboard.press("Escape")
await openSettings(page)
await expect(dialog.getByTestId("provider-row-p1")).toBeVisible()
})
test("deleting a provider says what goes with it", async ({ page }) => {
await openApp(page, { config: PROVIDERS })
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-p3").click()
await dialog
.getByRole("button", { name: "Delete DeepSeek and its model" })
.click()
await page
.getByRole("alertdialog")
.getByRole("button", { name: "Delete" })
.click()
await expect(dialog.getByTestId("provider-row-p3")).toHaveCount(0)
await expect(dialog.getByTestId("provider-row-p1")).toBeVisible()
expect((await savedModelConfigs(page)).providers).toHaveLength(2)
})
test("the general and drawing tabs hold their settings", async ({ page }) => {
await openApp(page)
await openSettingsTab(page, "general")
const dialog = settingsDialog(page)
for (const id of ["#language-select", "#send-shortcut-select"]) {
await expect(dialog.locator(id)).toBeVisible()
}
await expect(
dialog.getByRole("radiogroup", { name: "Theme" }),
).toBeVisible()
await dialog.getByTestId("settings-tab-drawing").click()
for (const id of [
"#custom-system-message",
"#vlm-validation",
"#max-output-tokens",
]) {
await expect(dialog.locator(id)).toBeVisible()
}
await expect(
dialog.getByRole("radiogroup", { name: "Diagram style" }),
).toBeVisible()
})
test("Add a provider… leaves the cursor in the provider search", async ({
page,
}) => {
await openApp(page)
await page.getByTestId("model-selector").click()
await page.getByText("Add a provider…").click()
const search = settingsDialog(page).getByRole("searchbox")
await expect(search).toBeFocused()
// After the model picker has closed
await page.waitForTimeout(800)
await expect(search).toBeFocused()
await page.keyboard.type("kimi")
await expect(search).toHaveValue("kimi")
})
test("the model picker's footer is there while searching and reachable by Tab", async ({
page,
}) => {
await openApp(page)
await page.getByTestId("model-selector").click()
await page.keyboard.type("zzzz")
await expect(page.getByText("Add a provider…")).toBeVisible()
const add = page.getByRole("button", { name: "Add a provider…" })
for (let i = 0; i < 6; i++) {
if (await add.evaluate((el) => el === document.activeElement)) break
await page.keyboard.press("Tab")
}
await expect(add).toBeFocused()
await page.keyboard.press("Enter")
await expect(settingsDialog(page).getByRole("searchbox")).toBeVisible()
})
test("an Enter that confirms an IME candidate picks no provider", async ({
page,
}) => {
await openApp(page)
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByRole("button", { name: /See all/ }).click()
const search = dialog.getByRole("searchbox")
await search.fill("deep")
await search.evaluate((el) =>
el.dispatchEvent(
new KeyboardEvent("keydown", {
key: "Enter",
bubbles: true,
isComposing: true,
}),
),
)
await page.waitForTimeout(300)
await expect(search).toBeVisible()
expect((await savedModelConfigs(page)).providers ?? []).toHaveLength(0)
})
test("the focus follows the page within the models tab", async ({ page }) => {
await openApp(page, { config: TWO_PROVIDERS })
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-p1").focus()
await page.keyboard.press("Enter")
await expect(
dialog.getByRole("heading", { name: "GLM (Zhipu)", exact: true }),
).toBeFocused()
await backToModels(dialog)
await expect(
dialog.getByRole("heading", { name: "Models", exact: true }),
).toBeFocused()
})
test("a passing test hands the focus to its next step, a failing one keeps it", async ({
page,
}) => {
let pass = false
await page.route("**/api/validate-model", (route) =>
route.fulfill({
json: pass
? { valid: true, responseTime: 900 }
: { valid: false, error: "Model not found" },
}),
)
await openApp(page, { config: TWO_PROVIDERS })
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-p1").click()
const testButton = dialog.getByTestId("test-models")
await testButton.focus()
await page.keyboard.press("Enter")
await expect(dialog.getByTestId("test-result")).toHaveText(
"1 model failed the test",
)
await expect(testButton).toBeFocused()
pass = true
await page.keyboard.press("Enter")
const use = dialog.getByRole("button", { name: "Use glm-a in the chat" })
await expect(use).toBeFocused()
const resultId = await dialog.getByTestId("test-result").getAttribute("id")
await expect(use).toHaveAttribute("aria-describedby", resultId ?? "")
})
test("the test result goes to a live region that is there from the start", async ({
page,
}) => {
await page.route("**/api/validate-model", (route) =>
route.fulfill({ json: { valid: true, responseTime: 900 } }),
)
await openApp(page, { config: TWO_PROVIDERS })
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-p1").click()
const result = dialog.getByTestId("test-result")
await expect(result).toHaveAttribute("role", "status")
await expect(result).toHaveText("")
await dialog.getByTestId("test-models").click()
await expect(result).toHaveText("glm-a works")
})
test("long names stay inside the page", async ({ page }) => {
const long = `model-${"x".repeat(150)}`
await openApp(page, {
config: {
version: 1,
providers: [
{
id: "p1",
provider: "openai",
name: `My ${"provider ".repeat(10)}name`,
apiKey: "k",
models: [{ id: "m1", modelId: long }],
},
],
},
})
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-p1").click()
const right = async (l: Locator) => {
const box = await l.boundingBox()
return box ? box.x + box.width : 0
}
const pageRight = await right(dialog)
expect(await right(dialog.getByTestId("test-models"))).toBeLessThanOrEqual(
pageRight,
)
expect(
await right(
dialog.getByRole("button", { name: /^Get all models from/ }),
),
).toBeLessThanOrEqual(pageRight)
})
test("a local server's base URL is shown from the start", async ({ page }) => {
await openApp(page, {
config: {
version: 1,
providers: [
{ id: "l1", provider: "ollama", apiKey: "", models: [] },
],
},
})
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-l1").click()
await expect(dialog.locator("#base-url")).toBeVisible()
})
test("Azure needs its resource URL before it counts as connected", async ({
page,
}) => {
// An entry saved while Azure still had a placeholder default URL: the
// placeholder names no resource, so the entry is not set up yet
await openApp(page, {
config: {
version: 1,
providers: [
{
id: "z1",
provider: "azure",
apiKey: "k",
baseUrl: "https://your-resource.openai.azure.com/openai",
models: [{ id: "m1", modelId: "gpt-6-astra" }],
},
],
},
})
await openSettings(page)
const dialog = settingsDialog(page)
await expect(dialog.getByTestId("provider-row-z1")).toContainText(
"Not set up yet",
)
await dialog.getByTestId("provider-row-z1").click()
await expect(dialog.getByTestId("step-1-todo")).toBeVisible()
await expect(dialog.getByTestId("test-models")).toBeDisabled()
await expect(
dialog.getByText("Fill in the API key and the base URL first."),
).toBeVisible()
await dialog
.locator("#base-url")
.fill("https://contoso.openai.azure.com/openai")
await expect(dialog.getByTestId("test-models")).toBeEnabled()
})
test("the OpenAI-compatible preset takes any URL and shows where requests go", async ({
page,
}) => {
await openApp(page)
await openSettings(page)
const dialog = settingsDialog(page)
// Not among the popular ones: it is in the full picker
await dialog.getByRole("button", { name: /^See all \d+ providers/ }).click()
await dialog.getByTestId("pick-provider-custom").click()
await expect(
dialog.getByRole("heading", { name: "OpenAI-compatible", exact: true }),
).toBeVisible()
// No URL yet: a key alone does not count as connected
await dialog.locator("#api-key").fill("any")
await expect(dialog.getByTestId("step-1-todo")).toBeVisible()
await expect(
dialog.getByText("Fill in the API key and the base URL first."),
).toBeVisible()
// Its options start open; the URL field shows the chat endpoint
await dialog.locator("#base-url").fill("http://127.0.0.1:1234/v1")
await expect(
dialog.getByText(
"Requests go to http://127.0.0.1:1234/v1/chat/completions",
),
).toBeVisible()
await expect(dialog.getByTestId("step-1-done")).toBeVisible()
})
test("a model id typed for one provider does not follow to the next", async ({
page,
}) => {
await openApp(page, { config: TWO_PROVIDERS })
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-p1").click()
const input = dialog.getByPlaceholder("Or type another model ID")
await input.fill("half-typed")
await backToModels(dialog)
await dialog.getByTestId("provider-row-p2").click()
await expect(input).toHaveValue("")
})
test("refused keys stay marked for each provider", async ({ page }) => {
await page.route("**/api/validate-model", (route) =>
route.fulfill({
json: { valid: false, code: "invalid_api_key", error: "Bad key" },
}),
)
await openApp(page, { config: TWO_PROVIDERS })
await openSettings(page)
const dialog = settingsDialog(page)
const key = dialog.locator("#api-key")
await dialog.getByTestId("provider-row-p1").click()
await dialog.getByTestId("test-models").click()
await expect(key).toHaveAttribute("aria-invalid", "true")
await backToModels(dialog)
await dialog.getByTestId("provider-row-p2").click()
await dialog.getByTestId("test-models").click()
await expect(key).toHaveAttribute("aria-invalid", "true")
await backToModels(dialog)
await dialog.getByTestId("provider-row-p1").click()
await expect(key).toHaveAttribute("aria-invalid", "true")
})
test("a model list that loads leaves a refused test's mark", async ({
page,
}) => {
// Some providers list their models without the key: a list proves nothing
await page.route("**/api/validate-model", (route) =>
route.fulfill({
json: { valid: false, code: "invalid_api_key", error: "Bad key" },
}),
)
await page.route("**/api/provider-models", (route) =>
route.fulfill({ json: { models: [{ id: "listed-model" }] } }),
)
await openApp(page, { config: TWO_PROVIDERS })
await openSettings(page)
const dialog = settingsDialog(page)
const key = dialog.locator("#api-key")
await dialog.getByTestId("provider-row-p1").click()
await dialog.getByTestId("test-models").click()
await expect(key).toHaveAttribute("aria-invalid", "true")
await dialog.getByRole("button", { name: /^Get all models from/ }).click()
await expect(
page.getByRole("option", { name: "listed-model" }),
).toBeVisible()
await page.keyboard.press("Escape")
await expect(page.getByRole("listbox")).toHaveCount(0)
await expect(key).toHaveAttribute("aria-invalid", "true")
})
test("an empty model list says so and opens nothing", async ({ page }) => {
let models: { id: string }[] = []
await page.route("**/api/provider-models", (route) =>
route.fulfill({ json: { models } }),
)
// OpenAI lists its own models: nothing to browse until a list arrives
await openApp(page, {
config: {
version: 1,
providers: [
{ id: "o1", provider: "openai", apiKey: "k", models: [] },
],
},
})
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-o1").click()
// A key the provider already had fetches nothing by itself
await page.waitForTimeout(800)
await expect(
dialog.getByRole("button", { name: "Browse models" }),
).toHaveCount(0)
await dialog.getByRole("button", { name: /^Get all models from/ }).click()
await expect(
dialog.getByText("The provider returned no models."),
).toBeVisible()
// The picker did not open by itself
await expect(page.getByRole("listbox")).toHaveCount(0)
// A new key fetches the list again by itself; the list waits behind
// "Browse models" and the cursor stays in the key field
models = [{ id: "fresh-model" }]
await dialog.locator("#api-key").fill("k-new")
await expect(
dialog.getByRole("button", { name: "Browse models" }),
).toBeVisible()
await expect(page.getByRole("listbox")).toHaveCount(0)
await expect(dialog.locator("#api-key")).toBeFocused()
})