npmUpdate.spec.ts 5.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  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. import { npmRuntimeDigest } from '../../bin/lib/npm-release.mjs'
  11. import { writeNpmFixture } from '../fixtures/npm-release.mjs'
  12. test('npm update downloads in place, survives reopening, and refreshes only after explicit server restart', async ({ page }, testInfo) => {
  13. const root = mkdtempSync(join(tmpdir(), 'yaoyao-npm-ui-')), home = join(root, 'data'), projectRoot = join(root, 'initial')
  14. const current = JSON.parse(readFileSync('release.json', 'utf8'))
  15. const version = current.webVersion.split('.').map((value: string, index: number) => index === 2 ? String(Number(value) + 1) : value).join('.')
  16. writeNpmFixture(projectRoot, current.webVersion)
  17. const releaseRoot = join(root, 'programs'), packageRoot = join(releaseRoot, 'releases/prepared/node_modules/@lsamien/yaoyao')
  18. const target = writeNpmFixture(packageRoot, version)
  19. const prepared = { root: packageRoot, digest: npmRuntimeDigest(packageRoot, version) }
  20. const { createApplication, createNodeServer } = await import(pathToFileURL(join(process.cwd(), 'dist-server/server/app.js')).href)
  21. const { SystemUpdateManager } = await import(pathToFileURL(join(process.cwd(), 'dist-server/server/updateManager.js')).href)
  22. const config = { host: '127.0.0.1', port: 0, upstream: new URL('http://127.0.0.1:1'), allowedHosts: new Set(['127.0.0.1']),
  23. home, mediaRoot: home, attachmentsRoot: home, imagesRoot: home, mediaOwner: 'test', allowInsecureLan: false, insecureLan: false, production: true,
  24. releaseRoot, releaseSource: 'npm:@lsamien/yaoyao' }
  25. let finishDownload!: () => void, downloads = 0, restarts = 0, confirmations = 0
  26. const updates = new SystemUpdateManager(config, { projectRoot, platform: 'darwin',
  27. inspectRemote: async () => ({ manifest: target, npm: { version, tarball: 'https://registry.npmjs.org/fixture.tgz', integrity: `sha512-${Buffer.alloc(64).toString('base64')}` } }),
  28. // Native archive installation and actual process replacement are covered by
  29. // verify-npm-update.mjs; this fixture exercises the real UI/API/state flow.
  30. launchUpdater: (path: string) => {
  31. const job = JSON.parse(readFileSync(path, 'utf8'))
  32. if (job.plan.prepared) {
  33. restarts++
  34. writeFileSync(join(projectRoot, 'release.json'), JSON.stringify(target))
  35. writeFileSync(path, JSON.stringify({ ...job, state: 'succeeded', message: '服务器更新完成' }))
  36. rmSync(join(home, 'updates/active.lock'))
  37. } else {
  38. downloads++
  39. writeFileSync(path, JSON.stringify({ ...job, state: 'downloading', received: 37, total: 100, message: '正在下载服务器更新 · 37%' }))
  40. finishDownload = () => {
  41. writeFileSync(path, JSON.stringify({ ...job, state: 'prepared', message: '新版本已准备完成,点击重启服务器生效', plan: { ...job.plan, prepared } }))
  42. rmSync(join(home, 'updates/active.lock'))
  43. }
  44. }
  45. },
  46. })
  47. const runtime = createApplication({ config, updates })
  48. runtime.app.use(serve(join(process.cwd(), 'dist')))
  49. runtime.app.use(async (ctx: Parameters<ReturnType<typeof serve>>[0]) => {
  50. if (ctx.method === 'GET' && ctx.accepts('html')) await send(ctx, 'index.html', { root: join(process.cwd(), 'dist') })
  51. })
  52. const node = createNodeServer(runtime); node.server.listen(0, '127.0.0.1'); await once(node.server, 'listening')
  53. const origin = `http://127.0.0.1:${(node.server.address() as AddressInfo).port}`
  54. async function openUpdates() {
  55. await page.getByRole('button', { name: /^(打开我的设置|设置与模式)$/ }).click()
  56. await page.getByRole('menuitem', { name: '我的设置', exact: true }).click()
  57. const dialog = page.getByRole('dialog', { name: '我的设置' })
  58. await dialog.getByRole('button', { name: '更新与回滚', exact: true }).click()
  59. return dialog
  60. }
  61. try {
  62. const boot = await (await page.request.get(origin + '/api/app/bootstrap')).json()
  63. const signedIn = await page.request.post(origin + '/api/app/setup', { headers: { Origin: origin, 'X-CSRF-Token': boot.csrfToken }, data: { username: 'admin', password: 'npm-e2e-password' } })
  64. expect(signedIn.ok()).toBe(true)
  65. page.on('dialog', dialog => { confirmations++; void dialog.dismiss() })
  66. await page.goto(origin + '/chat')
  67. let dialog = await openUpdates()
  68. await dialog.getByRole('button', { name: '下载更新', exact: true }).click()
  69. await expect(dialog.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '37')
  70. expect(downloads).toBe(1); expect(restarts).toBe(0)
  71. await page.screenshot({ path: testInfo.outputPath('npm-downloading.png') })
  72. finishDownload()
  73. await expect(dialog.getByRole('button', { name: '重启服务器', exact: true })).toBeEnabled()
  74. await page.screenshot({ path: testInfo.outputPath('npm-ready.png') })
  75. await page.reload()
  76. dialog = await openUpdates()
  77. await expect(dialog.getByRole('button', { name: '重启服务器', exact: true })).toBeEnabled()
  78. expect(restarts).toBe(0)
  79. const refreshed = page.waitForEvent('framenavigated', { predicate: frame => frame === page.mainFrame() })
  80. await dialog.getByRole('button', { name: '重启服务器', exact: true }).click()
  81. await refreshed
  82. expect(restarts).toBe(1); expect(confirmations).toBe(0)
  83. const status = await (await page.request.get(origin + '/api/app/system/update/status')).json()
  84. expect(status.current.webVersion).toBe(version)
  85. expect(status.job.state).toBe('succeeded')
  86. } finally {
  87. await page.goto('about:blank')
  88. node.server.closeAllConnections(); await node.close()
  89. rmSync(root, { recursive: true, force: true })
  90. }
  91. })