test(web): cover the plugin configuration section end to end
Three scenarios over the real wire: the cards this deployment's exposed namespaces produce, one field edited down to the settings document with the override badge that layering produces, and the reset that clears it back to the composed default. Every other settings golden gains the same nav entry and nothing else.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
// Web e2e scenario: the Plugins settings section — the cards a deployment's
|
||||
// exposed host-plane namespaces produce, one field edited through the real
|
||||
// wire down to `$DSH_HOME/settings.yaml`, and the override badge and reset
|
||||
// that layering produces. Zero model calls: everything is client state plus
|
||||
// the settings document on a blank frame, so there is no fixture and a stray
|
||||
// stream would fail loud on the open llm seam.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/plugin-config', import.meta.url))
|
||||
const SECTION_EXPECTED = join(SNAPSHOT_DIR, 'section.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
describe('web e2e: plugin configuration section', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
browser = await chromium.launch()
|
||||
// Chinese browser: the section asserts the localized copy the client
|
||||
// derives from it, as the rest of the settings surface does.
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
/**
|
||||
* Open the settings dialog on the Plugins section. The scenarios share one
|
||||
* page so the settings document accumulates across them, so this leaves any
|
||||
* dialog a previous scenario opened closed first — its mask would otherwise
|
||||
* swallow the trigger click.
|
||||
*/
|
||||
async function openPlugins() {
|
||||
if (await page.getByRole('dialog', { name: '设置' }).count() > 0) {
|
||||
await page.keyboard.press('Escape')
|
||||
await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0)
|
||||
}
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: '插件' }).click()
|
||||
await expect
|
||||
.poll(() => dialog.getByRole('button', { name: '插件' }).getAttribute('aria-current'), { timeout: 5_000 })
|
||||
.toBe('true')
|
||||
return dialog
|
||||
}
|
||||
|
||||
/** The settings document as the Host has written it so far. */
|
||||
async function settingsDocument(): Promise<string> {
|
||||
return readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8').catch(() => '')
|
||||
}
|
||||
|
||||
it('shows one card per exposed host-plane namespace', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-config-cards'))
|
||||
const dialog = await openPlugins()
|
||||
|
||||
// Every card the shipped web composition exposes: the shell executor, the
|
||||
// agent loop, and the DeepSeek search provider.
|
||||
await dialog.getByText('终端', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
expect(await dialog.getByText('Agent 循环', { exact: true }).count()).toBe(1)
|
||||
expect(await dialog.getByText('网页搜索', { exact: true }).count()).toBe(1)
|
||||
// Collapsed: a card's fields appear only once it is expanded.
|
||||
expect(await dialog.getByLabel('命令超时(毫秒)').count()).toBe(0)
|
||||
|
||||
const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(SECTION_EXPECTED, snapshot, MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('writes an edited field to the settings document and marks it overridden', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-config-write'))
|
||||
const dialog = await openPlugins()
|
||||
await dialog.getByText('终端', { exact: true }).click()
|
||||
|
||||
const timeout = dialog.getByLabel('命令超时(毫秒)')
|
||||
await timeout.waitFor({ timeout: 10_000 })
|
||||
// The composed default this deployment ships, before any user layer.
|
||||
expect(await timeout.inputValue()).toBe('60000')
|
||||
await timeout.fill('12000')
|
||||
await timeout.blur()
|
||||
|
||||
await expect.poll(async () => (await settingsDocument()).includes('timeoutMs: 12000'), { timeout: 10_000 })
|
||||
.toBe(true)
|
||||
// Presence in the user layer is what the badge reports, and the reset is
|
||||
// offered only for a field that has one.
|
||||
await expect.poll(() => dialog.getByText('已覆盖').count(), { timeout: 5_000 }).toBe(1)
|
||||
expect(await dialog.getByRole('button', { name: '恢复默认' }).count()).toBe(1)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('clears the field back to the composed default on reset', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-config-reset'))
|
||||
const dialog = await openPlugins()
|
||||
await dialog.getByText('终端', { exact: true }).click()
|
||||
const timeout = dialog.getByLabel('命令超时(毫秒)')
|
||||
await timeout.waitFor({ timeout: 10_000 })
|
||||
expect(await timeout.inputValue()).toBe('12000')
|
||||
|
||||
await dialog.getByRole('button', { name: '恢复默认' }).click()
|
||||
|
||||
await expect.poll(async () => (await settingsDocument()).includes('timeoutMs'), { timeout: 10_000 })
|
||||
.toBe(false)
|
||||
await expect.poll(() => timeout.inputValue(), { timeout: 5_000 }).toBe('60000')
|
||||
expect(await dialog.getByText('已覆盖').count()).toBe(0)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['section.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -10,6 +10,9 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件":
|
||||
- img
|
||||
- text: 插件
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件":
|
||||
- img
|
||||
- text: 插件
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件":
|
||||
- img
|
||||
- text: 插件
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件":
|
||||
- img
|
||||
- text: 插件
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件":
|
||||
- img
|
||||
- text: 插件
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件":
|
||||
- img
|
||||
- text: 插件
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件":
|
||||
- img
|
||||
- text: 插件
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
- dialog "设置":
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置":
|
||||
- img
|
||||
- text: 通用设置
|
||||
- button "模型":
|
||||
- img
|
||||
- text: 模型
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件":
|
||||
- img
|
||||
- text: 插件
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
- text: 关闭
|
||||
- heading "插件配置" [level=2]
|
||||
- paragraph: 本部署所组装插件自己拥有的设置。你在这里设的值会覆盖组装默认值,并在下一次使用时生效。
|
||||
- list:
|
||||
- listitem:
|
||||
- button "终端 限制 agent 运行的每一条命令。":
|
||||
- img
|
||||
- text: 终端 限制 agent 运行的每一条命令。
|
||||
- listitem:
|
||||
- button "Agent 循环 Agent 如何派发工具调用。":
|
||||
- img
|
||||
- text: Agent 循环 Agent 如何派发工具调用。
|
||||
- listitem:
|
||||
- button "网页搜索 DeepSeek 搜索提供方。":
|
||||
- img
|
||||
- text: 网页搜索 DeepSeek 搜索提供方。
|
||||
@@ -10,6 +10,9 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件":
|
||||
- img
|
||||
- text: 插件
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"tests/trajectory-virtualization.e2e.ts",
|
||||
"tests/lifecycle-chrome.e2e.ts",
|
||||
"tests/details-session-lifecycle.e2e.ts",
|
||||
"tests/plugin-config.e2e.ts",
|
||||
"tests/settings-chrome.e2e.ts",
|
||||
"tests/models-settings.e2e.ts",
|
||||
"tests/default-model.e2e.ts",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"apps/web/tests/trajectory-virtualization.e2e.ts",
|
||||
"apps/web/tests/lifecycle-chrome.e2e.ts",
|
||||
"apps/web/tests/details-session-lifecycle.e2e.ts",
|
||||
"apps/web/tests/plugin-config.e2e.ts",
|
||||
"apps/web/tests/settings-chrome.e2e.ts",
|
||||
"apps/web/tests/models-settings.e2e.ts",
|
||||
"apps/web/tests/onboarding-deepseek-config.e2e.ts",
|
||||
|
||||
Reference in New Issue
Block a user