lifecycle.test.mjs 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import { mkdtemp, readFile, rm, mkdir } from 'node:fs/promises'
  4. import { tmpdir } from 'node:os'
  5. import { join, resolve } from 'node:path'
  6. import { createServer } from 'node:net'
  7. import { spawn } from 'node:child_process'
  8. import { _electron as electron } from '@playwright/test'
  9. import { localJSON, DesktopServiceManager } from './service-manager.mjs'
  10. const root = resolve(import.meta.dirname, '..')
  11. async function freePort() {
  12. const server = createServer(); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
  13. const port = server.address().port; await new Promise(resolve => server.close(resolve));return port
  14. }
  15. async function until(read, accept, timeout = 30000) {
  16. const end = Date.now() + timeout;let value
  17. while (Date.now() < end) {
  18. try { value = await read(); if (accept(value)) return value } catch {}
  19. await new Promise(resolve => setTimeout(resolve, 100))
  20. }
  21. throw new Error('等待验收状态超时')
  22. }
  23. test('real desktop launches its bundled service, survives window close and recovers its own crashed child', { timeout: 90000 }, async () => {
  24. const home = await mkdtemp(join(tmpdir(), 'yaoyao-desktop-e2e-'))
  25. const port = await freePort(), upstreamPort = await freePort()
  26. const upstream = spawn(process.execPath, [join(root, 'tests/fixtures/fake-hermes.mjs')], {
  27. cwd: root, env: { ...process.env, FAKE_HERMES_PORT: String(upstreamPort), FAKE_HERMES_LOCAL_AUTH: '1' }, stdio: 'ignore',
  28. })
  29. let desktop
  30. let servicePID
  31. try {
  32. await until(() => localJSON(`http://127.0.0.1:${upstreamPort}/api/status`), Boolean)
  33. desktop = await electron.launch({ ...(process.env.DESKTOP_TEST_EXECUTABLE ? { executablePath: process.env.DESKTOP_TEST_EXECUTABLE } : {}),
  34. args: process.env.DESKTOP_TEST_EXECUTABLE ? [] : [root], cwd: root, env: { ...process.env,
  35. HERMES_YAOYAO_DESKTOP_TEST_HOME: home, HERMES_YAOYAO_DESKTOP_PORT: String(port),
  36. HERMES_YAOYAO_UPSTREAM: `http://127.0.0.1:${upstreamPort}` }, timeout: 30000 })
  37. desktop.process().stderr.on('data', b => { if (process.env.DESKTOP_TEST_LOGS) process.stderr.write(b) })
  38. const page = await desktop.firstWindow()
  39. await page.waitForURL(`http://127.0.0.1:${port}/**`, { timeout: 30000 })
  40. const owner = await until(async () => JSON.parse(await readFile(join(home, 'service-instance.json'), 'utf8')), r => r.url)
  41. servicePID = owner.pid
  42. const identity = await localJSON(`http://127.0.0.1:${port}/desktop/service`, { 'x-yaoyao-desktop-token': owner.token })
  43. assert.equal(identity.pid, owner.pid);assert.equal(identity.desktopOwned, true)
  44. assert.equal(identity.token, undefined)
  45. assert.ok((await localJSON(`http://127.0.0.1:${port}/healthz`)).ok)
  46. const denied = await page.evaluate(async () => {
  47. try { await window.yaoyaoDesktop.retry();return false } catch { return true }
  48. })
  49. assert.equal(denied, true, 'the web app must not have the boot screen native control privilege')
  50. await desktop.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].close())
  51. assert.equal(await desktop.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].isVisible()), false)
  52. assert.ok((await localJSON(`http://127.0.0.1:${port}/healthz`)).ok)
  53. await desktop.evaluate(({ app }) => app.emit('activate'))
  54. assert.equal(await desktop.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].isVisible()), true)
  55. await mkdir(join(root, 'test-results/desktop'), { recursive: true })
  56. await page.screenshot({ path: join(root, 'test-results/desktop/first-launch.png') })
  57. process.kill(owner.pid, 'SIGKILL')
  58. const replacement = await until(async () => JSON.parse(await readFile(join(home, 'service-instance.json'), 'utf8')), r => r.pid !== owner.pid && r.url)
  59. servicePID = replacement.pid
  60. await until(() => localJSON(`http://127.0.0.1:${port}/healthz`), r => r.ok)
  61. assert.notEqual(replacement.instanceId, owner.instanceId)
  62. const external = new DesktopServiceManager({ home, port, fork: () => { throw new Error('must not fork a second service') } })
  63. assert.equal((await external.start()).external, true)
  64. await external.stop()
  65. assert.ok((await localJSON(`http://127.0.0.1:${port}/healthz`)).ok, 'a connected client must not stop the owned service')
  66. await desktop.close();desktop = null
  67. await until(async () => { try { process.kill(replacement.pid, 0);return false } catch { return true } }, Boolean, 10000)
  68. } finally {
  69. await desktop?.close().catch(() => {})
  70. if (servicePID) { try { process.kill(servicePID, 'SIGTERM') } catch {} }
  71. upstream.kill('SIGTERM')
  72. await rm(home, { recursive: true, force: true })
  73. }
  74. })