1
0

browser-open.spec.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. /** Default-browser startup over a real Loader tree and listening Web server. */
  2. import { mkdirSync, mkdtempSync, 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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  7. import { Context } from '@deepseek-ai/cordis'
  8. import Include from '@deepseek-ai/cordis-plugin-include'
  9. import Loader from '@deepseek-ai/cordis-plugin-loader'
  10. import WebServer from '@deepseek-ai/dsh-host-webserver'
  11. import { apply, internals } from '../src/index.ts'
  12. const contexts: Context[] = []
  13. const tempRoots: string[] = []
  14. const originalResolveDistIndex = internals.resolveDistIndex
  15. const originalOpenBrowser = internals.openBrowser
  16. beforeEach(() => {
  17. vi.stubEnv('SSH_CONNECTION', '')
  18. vi.stubEnv('SSH_TTY', '')
  19. })
  20. afterEach(async () => {
  21. for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
  22. for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true })
  23. internals.resolveDistIndex = originalResolveDistIndex
  24. internals.openBrowser = originalOpenBrowser
  25. vi.unstubAllEnvs()
  26. Reflect.deleteProperty(globalThis, '__dshWebAppApply')
  27. Reflect.deleteProperty(globalThis, '__dshWebServer')
  28. Reflect.deleteProperty(globalThis, '__dshConnection')
  29. })
  30. describe('web app browser startup', () => {
  31. it('opens the canonical URL only after the complete page is reachable', async () => {
  32. const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-'))
  33. tempRoots.push(root)
  34. const dist = join(root, 'dist')
  35. mkdirSync(dist)
  36. const index = join(dist, 'index.html')
  37. writeFileSync(index, '<!doctype html><title>ready</title>')
  38. internals.resolveDistIndex = () => index
  39. const webserverModule = join(root, 'webserver.mjs')
  40. const connectionModule = join(root, 'connection.mjs')
  41. const webAppModule = join(root, 'web-app.mjs')
  42. writeFileSync(webserverModule, 'export default globalThis.__dshWebServer\n')
  43. writeFileSync(connectionModule, [
  44. "export const inject = ['webServer']",
  45. "export const apply = ctx => ctx.provide('connection', globalThis.__dshConnection)",
  46. '',
  47. ].join('\n'))
  48. writeFileSync(webAppModule, [
  49. "export const name = 'fixture-web-app'",
  50. "export const inject = ['webServer']",
  51. 'export const apply = (ctx, config) => globalThis.__dshWebAppApply(ctx, config)',
  52. '',
  53. ].join('\n'))
  54. const config = join(root, 'cordis.yml')
  55. writeFileSync(config, [
  56. '- id: webserver',
  57. ` name: ${pathToFileURL(webserverModule).href}`,
  58. ' config:',
  59. ' host: 127.0.0.1',
  60. ' port: 0',
  61. '- id: connection',
  62. ` name: ${pathToFileURL(connectionModule).href}`,
  63. '- id: web-app',
  64. ` name: ${pathToFileURL(webAppModule).href}`,
  65. ' config:',
  66. ' openBrowser: true',
  67. ' printUrl: false',
  68. ' surfaceContext: false',
  69. ' trustedHosts: []',
  70. '',
  71. ].join('\n'))
  72. const globals = globalThis as unknown as {
  73. __dshWebAppApply: typeof apply
  74. __dshWebServer: typeof WebServer
  75. __dshConnection: {
  76. authenticatedUrl(baseUrl: string): string
  77. authorizeIndex(): boolean
  78. requestRejection(): undefined
  79. rpc: object
  80. }
  81. }
  82. globals.__dshWebAppApply = apply
  83. globals.__dshWebServer = WebServer
  84. globals.__dshConnection = {
  85. authenticatedUrl: (baseUrl) => {
  86. const url = new URL(baseUrl)
  87. url.searchParams.set('token', 'fixture-token')
  88. return url.href
  89. },
  90. authorizeIndex: () => true,
  91. requestRejection: () => undefined,
  92. rpc: {},
  93. }
  94. let openedUrl: string | undefined
  95. let openedStatus: number | undefined
  96. let resolveOpened!: () => void
  97. const opened = new Promise<void>((resolve) => { resolveOpened = resolve })
  98. internals.openBrowser = async (url) => {
  99. openedUrl = url
  100. openedStatus = (await fetch(url)).status
  101. resolveOpened()
  102. }
  103. const ctx = new Context()
  104. contexts.push(ctx)
  105. await ctx.plugin(Loader)
  106. ctx.loader.builtins.include = Include
  107. await ctx.loader.create({
  108. name: 'cordis:include',
  109. config: { path: pathToFileURL(config).href },
  110. })
  111. await ctx.loader.await()
  112. await opened
  113. expect(openedUrl).toBe(`http://127.0.0.1:${String(ctx.webServer.port)}/?token=fixture-token`)
  114. expect(openedStatus).toBe(200)
  115. })
  116. })