offlineUpdate.spec.ts 4.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. import { expect, test } from '@playwright/test'
  2. import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { pathToFileURL } from 'node:url'
  6. import { once } from 'node:events'
  7. import type { AddressInfo } from 'node:net'
  8. import serve from 'koa-static'
  9. import send from 'koa-send'
  10. test('opens settings and schedules an independent Web update while all 9119 requests fail', async ({ page }, testInfo) => {
  11. const home = mkdtempSync(join(tmpdir(), 'yaoyao-offline-ui-'))
  12. const { createApplication, createNodeServer } = await import(pathToFileURL(join(process.cwd(), 'dist-server/server/app.js')).href)
  13. const { SystemUpdateManager } = await import(pathToFileURL(join(process.cwd(), 'dist-server/server/updateManager.js')).href)
  14. const current = JSON.parse(readFileSync('release.json', 'utf8'))
  15. const [major, minor, patch] = current.webVersion.split('.').map(Number)
  16. const version = `${major}.${minor}.${patch + 1}`
  17. const latest = { ...current, releaseVersion: version, webVersion: version, gitTag: `v${version}` }
  18. const config = {
  19. host: '127.0.0.1', port: 0, upstream: new URL('http://127.0.0.1:1'),
  20. allowedHosts: new Set(['127.0.0.1']), home, mediaRoot: home, attachmentsRoot: home, imagesRoot: home,
  21. mediaOwner: 'test', allowInsecureLan: false, insecureLan: false, production: true,
  22. }
  23. const launched: string[] = []
  24. const completedJobs: string[] = []
  25. const updates = new SystemUpdateManager(config, {
  26. projectRoot: process.cwd(), platform: 'darwin',
  27. inspectRemote: async () => ({ manifest: latest, commit: 'b'.repeat(40) }),
  28. // Exercise scheduling and polling only; never install or stop a real service.
  29. launchUpdater: (path: string) => {
  30. const job = JSON.parse(readFileSync(path, 'utf8'))
  31. launched.push(job.target.releaseVersion)
  32. writeFileSync(path, JSON.stringify({ ...job, state: 'succeeded', message: '离线 Web 升级任务验收完成' }))
  33. completedJobs.push(path)
  34. rmSync(join(home, 'updates', 'active.lock'))
  35. },
  36. })
  37. const runtime = createApplication({ config, updates })
  38. runtime.app.use(serve(join(process.cwd(), 'dist')))
  39. runtime.app.use(async (ctx: Parameters<ReturnType<typeof serve>>[0]) => {
  40. if (ctx.method === 'GET' && ctx.accepts('html')) await send(ctx, 'index.html', { root: join(process.cwd(), 'dist') })
  41. })
  42. const node = createNodeServer(runtime)
  43. node.server.listen(0, '127.0.0.1'); await once(node.server, 'listening')
  44. const origin = `http://127.0.0.1:${(node.server.address() as AddressInfo).port}`
  45. try {
  46. const bootstrap = await (await page.request.get(origin + '/api/app/bootstrap')).json()
  47. const login = await page.request.post(origin + '/api/app/setup', {
  48. headers: { Origin: origin, 'X-CSRF-Token': bootstrap.csrfToken }, data: { username: 'admin', password: 'offline-e2e-password' },
  49. })
  50. const signedIn = await login.json()
  51. expect(login.ok(), `${login.status()} ${signedIn.error || ''}`).toBe(true)
  52. expect((await page.request.get(origin + '/readyz')).status()).toBe(503)
  53. await page.goto(origin + '/chat', { waitUntil: 'domcontentloaded' })
  54. await page.getByRole('button', { name: /^(打开我的设置|设置与模式)$/ }).click()
  55. await page.getByRole('menuitem', { name: '我的设置', exact: true }).click()
  56. const dialog = page.getByRole('dialog', { name: '我的设置' })
  57. await dialog.getByRole('button', { name: '系统概览', exact: true }).click()
  58. await expect(dialog.getByText(`Web ${current.webVersion} · 可独立升级与回滚`)).toBeVisible()
  59. await dialog.getByRole('button', { name: '更新与回滚', exact: true }).click()
  60. await expect(dialog.getByText(version, { exact: true })).toBeVisible()
  61. await expect(dialog.getByRole('alert')).toHaveCount(0)
  62. await expect(page.locator('.settings-center-layer')).toHaveCSS('opacity', '1')
  63. await page.screenshot({ path: testInfo.outputPath('offline-web-update.png') })
  64. page.once('dialog', confirmation => confirmation.accept())
  65. await dialog.getByRole('button', { name: '升级 Web', exact: true }).click()
  66. await expect.poll(() => completedJobs.length).toBe(1)
  67. expect(JSON.parse(readFileSync(completedJobs[0]!, 'utf8'))).toMatchObject({ state: 'succeeded', target: { webVersion: version } })
  68. await expect(dialog.getByRole('region', { name: '更新与回滚', exact: true })).toBeVisible()
  69. expect(launched).toEqual([version])
  70. } finally {
  71. await page.goto('about:blank')
  72. node.server.closeAllConnections()
  73. await node.close()
  74. rmSync(home, { recursive: true, force: true })
  75. }
  76. })