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.
131 lines
6.0 KiB
TypeScript
131 lines
6.0 KiB
TypeScript
// 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'])
|
|
})
|
|
})
|