plugin-install-cancel.e2e.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. // A real Host/Remote/browser composition with a controllable package-manager process.
  2. // No model call is needed: installation state and profile files are the observable result.
  3. import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises'
  4. import { tmpdir } from 'node:os'
  5. import { join } from 'node:path'
  6. import { fileURLToPath } from 'node:url'
  7. import { chromium } from 'playwright'
  8. import { expect, it } from 'vitest'
  9. import { launchWebScaffold, captureStableAria, compareOrRefreshGolden, webSnapshotMode, watchConsole, type WebScaffold } from './scaffold.ts'
  10. import { ZH_BROWSER_LOCALE } from './support.ts'
  11. it('cancels installation through the UI, restores files, and accepts a retry', async () => {
  12. const scratch = await mkdtemp(join(tmpdir(), 'dsh-install-cancel-'))
  13. const overlay = join(scratch, 'cordis.patch.yml')
  14. await writeFile(overlay, `- id: plugin-manager\n config: ${JSON.stringify({ pnpmCommand: process.execPath, installTimeoutMs: 60_000, installKillGraceMs: 50 })}\n`)
  15. let scaffold: WebScaffold | undefined
  16. try {
  17. scaffold = await launchWebScaffold({ profileRuntime: { packages: [] }, extraOverlayPath: overlay })
  18. const browser = await chromium.launch()
  19. try {
  20. const profile = join(scaffold.harnessHome, 'profiles', 'scaffold')
  21. const manifestPath = join(profile, 'package.json')
  22. const manifest = await readFile(manifestPath, 'utf8')
  23. const lockPath = join(profile, 'pnpm-lock.yaml')
  24. await writeFile(lockPath, 'original lockfile\n')
  25. // Node stands in for the pnpm executable; the same installer owns and stops this real child.
  26. await writeFile(join(profile, 'add'), `
  27. import('node:fs').then(fs => {
  28. fs.writeFileSync('package.json', JSON.stringify({ ...JSON.parse(fs.readFileSync('package.json', 'utf8')), dependencies: { partial: '1.0.0' } }));
  29. fs.writeFileSync('pnpm-lock.yaml', 'partial lockfile');
  30. console.log('Waiting for package download');
  31. setInterval(() => {}, 1000);
  32. });
  33. `)
  34. const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, locale: ZH_BROWSER_LOCALE })
  35. const tripwire = watchConsole(page)
  36. await page.goto(scaffold.authenticatedUrl)
  37. await page.waitForSelector('[class*="frame"]')
  38. if (await page.getByRole('dialog', { name: '设置' }).count() > 0) await page.keyboard.press('Escape')
  39. await page.getByRole('navigation', { name: '全局面板' }).getByRole('button', { name: '插件', exact: true }).click()
  40. const panel = page.locator('[data-plugin-panel]')
  41. await panel.getByRole('button', { name: '添加插件', exact: true }).click()
  42. const dialog = page.getByRole('dialog', { name: '添加插件' })
  43. await dialog.getByRole('textbox').fill('slow-package')
  44. await dialog.getByRole('button', { name: '安装', exact: true }).click()
  45. await dialog.getByText('Waiting for package download', { exact: true }).waitFor()
  46. await dialog.getByRole('button', { name: '取消安装', exact: true }).click()
  47. await dialog.getByText('已取消安装,本次未继续启用插件。下载缓存或已解包文件可能保留,需要时可重新安装。', { exact: true }).waitFor()
  48. expect(await readFile(manifestPath, 'utf8')).toBe(manifest)
  49. expect(await readFile(lockPath, 'utf8')).toBe('original lockfile\n')
  50. expect(await dialog.getByRole('textbox').inputValue()).toBe('slow-package')
  51. const snapshot = (await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd))
  52. .split(process.execPath).join('{{node}}')
  53. .split(scaffold.harnessHome).join('{{harnessHome}}')
  54. await compareOrRefreshGolden(fileURLToPath(new URL('./expected/plugin-install-cancel/cancelled.expected.md', import.meta.url)), snapshot, webSnapshotMode())
  55. await writeFile(join(profile, 'add'), 'console.log("Retry completed")\n')
  56. await dialog.getByRole('button', { name: '重试', exact: true }).click()
  57. await dialog.getByText('Retry completed', { exact: true }).waitFor()
  58. await expect.poll(() => dialog.getByRole('button', { name: '完成', exact: true }).count()).toBeGreaterThan(0)
  59. expect(tripwire.pageErrors).toEqual([])
  60. } finally { await browser.close() }
  61. } finally {
  62. await scaffold?.close()
  63. await rm(scratch, { recursive: true, force: true })
  64. }
  65. })