access-confirmation.e2e.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. // Web e2e scenario: every visible permission picker gates Full access behind
  2. // the same locale-aware, in-page risk confirmation. Zero model calls: the
  3. // scenario boots the shipped Web composition and exercises the real
  4. // permission projection, client command path, HTTP RPC, and pushed update.
  5. import { mkdirSync } from 'node:fs'
  6. import { fileURLToPath } from 'node:url'
  7. import { join } from 'node:path'
  8. import type { Browser, Page } from 'playwright'
  9. import { chromium } from 'playwright'
  10. import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
  11. import {
  12. assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
  13. launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
  14. } from './scaffold.ts'
  15. import { ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
  16. /**
  17. * connectFreshWorkspace twin over the product default Chinese locale (the
  18. * shared helper's anchors assume the English page every other scenario
  19. * boots; this scenario deliberately keeps zh, so the localized picker
  20. * copy is the anchor set).
  21. */
  22. async function connectFreshWorkspaceZh(page: Page, root: string, name = 'workspace'): Promise<void> {
  23. mkdirSync(join(root, name), { recursive: true })
  24. await page.getByRole('button', { name: '选择工作区' }).click()
  25. const dialog = page.getByRole('dialog', { name: '选择工作区目录' })
  26. await dialog.waitFor({ timeout: 10_000 })
  27. await dialog.getByRole('button', { name: '编辑路径' }).click()
  28. const pathInput = dialog.getByRole('textbox', { name: '编辑路径' })
  29. await pathInput.fill(join(root, name))
  30. await pathInput.press('Enter')
  31. await dialog.getByRole('button', { name: '打开', exact: true }).click()
  32. await page.locator('textarea:enabled[placeholder="描述你想要构建的内容"]')
  33. .waitFor({ timeout: 15_000 })
  34. }
  35. const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/access-confirmation', import.meta.url))
  36. const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
  37. const MODE = webSnapshotMode()
  38. describe('web e2e: Full access confirmation', () => {
  39. let scaffold: WebScaffold
  40. let browser: Browser
  41. let page: Page
  42. let tripwire: ReturnType<typeof watchConsole>
  43. beforeAll(async () => {
  44. scaffold = await launchWebScaffold({})
  45. // CI uses Playwright's pinned browser. A developer may point this one
  46. // scenario at an installed Chromium when the matching browser download
  47. // is temporarily unavailable.
  48. const executablePath = process.env.DSH_PLAYWRIGHT_EXECUTABLE_PATH
  49. browser = await chromium.launch(executablePath === undefined ? {} : { executablePath })
  50. // Keep the product default Chinese locale: the golden pins the actual
  51. // registered dictionary rather than a test-local translation callback.
  52. page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
  53. tripwire = watchConsole(page)
  54. await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
  55. await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
  56. await connectFreshWorkspaceZh(page, scaffold.workspaceCwd)
  57. }, 120_000)
  58. afterAll(async () => {
  59. await browser?.close()
  60. await scaffold?.close()
  61. })
  62. it('requires acknowledgement before the composer picker can enable Full access', async () => {
  63. onTestFailed(() => saveFailureShot(page, 'web-e2e-full-access-confirmation'))
  64. const access = page.locator('button[aria-label^="访问模式"]').first()
  65. await access.waitFor({ timeout: 10_000 })
  66. expect(await access.getAttribute('aria-label')).toBe('访问模式,当前:Workspace Write')
  67. await access.click()
  68. await page.getByRole('menuitem', { name: 'Full access' }).click()
  69. const dialog = page.getByRole('dialog', { name: '确认启用 Full access?' })
  70. await dialog.waitFor({ timeout: 10_000 })
  71. const enable = dialog.getByRole('button', { name: '启用 Full access' })
  72. expect(await enable.isDisabled()).toBe(true)
  73. // The modal is in this page's body (not a native/new window) and escapes
  74. // the sticky composer's stacking context.
  75. expect(await dialog.evaluate(node => node.parentElement?.parentElement === document.body)).toBe(true)
  76. const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
  77. await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
  78. await dialog.getByRole('checkbox', { name: '我已了解风险,并愿意继续' }).check()
  79. expect(await enable.isEnabled()).toBe(true)
  80. await enable.click()
  81. await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 })
  82. .toBe('访问模式,当前:Full access')
  83. expect(await dialog.count()).toBe(0)
  84. expect(tripwire.pageErrors).toEqual([])
  85. }, 60_000)
  86. it('keeps its snapshot inventory closed', async () => {
  87. expect(tripwire.warnings).toEqual([])
  88. await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
  89. })
  90. })