load-bundle.spec.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. // @vitest-environment jsdom
  2. import { afterEach, expect, it, vi } from 'vitest'
  3. import { WorkerTunnel } from '../../src/client/client.ts'
  4. type StubListener = (event: { data?: unknown }) => void
  5. function stubWorker(): {
  6. worker: Worker
  7. sent: { t: string; id: number; url: string }[]
  8. deliver: (frame: unknown) => void
  9. } {
  10. const listeners: StubListener[] = []
  11. const sent: { t: string; id: number; url: string }[] = []
  12. return {
  13. worker: {
  14. addEventListener: (type: string, listener: StubListener) => {
  15. if (type === 'message') listeners.push(listener)
  16. },
  17. postMessage: (frame: unknown) => { sent.push(frame as { t: string; id: number; url: string }) },
  18. } as unknown as Worker,
  19. sent,
  20. deliver: (frame) => { for (const listener of listeners) listener({ data: frame }) },
  21. }
  22. }
  23. afterEach(() => {
  24. vi.restoreAllMocks()
  25. vi.unstubAllGlobals()
  26. document.head.innerHTML = ''
  27. })
  28. it('loads a combo map through the tunnel and embeds it in the blob script', async () => {
  29. const { worker, sent, deliver } = stubWorker()
  30. const tunnel = new WorkerTunnel(worker)
  31. const blobs: Blob[] = []
  32. const revoked: string[] = []
  33. const NativeURL = URL
  34. class StubURL extends NativeURL {
  35. static override createObjectURL(blob: Blob): string {
  36. blobs.push(blob)
  37. return `blob:fixture-${String(blobs.length)}`
  38. }
  39. static override revokeObjectURL(url: string): void {
  40. revoked.push(url)
  41. }
  42. }
  43. vi.stubGlobal('URL', StubURL)
  44. vi.spyOn(document.head, 'append').mockImplementation((...nodes) => {
  45. for (const node of nodes) {
  46. if (typeof node !== 'string') queueMicrotask(() => { node.dispatchEvent(new Event('load')) })
  47. }
  48. })
  49. const scriptUrl = '/plugins/??a/client.js,b/client.js&rev=abc'
  50. const mapUrl = '/plugins/??a/client.js.map,b/client.js.map&rev=abc'
  51. const loading = tunnel.loadBundle(scriptUrl)
  52. expect(sent[0]?.url).toBe(`http://localhost:3000${scriptUrl}`)
  53. deliver({
  54. t: 'res',
  55. id: 1,
  56. status: 200,
  57. headers: { 'content-type': 'text/javascript' },
  58. body: new TextEncoder().encode(`factory();\n//# sourceMappingURL=${mapUrl}\n`).buffer,
  59. })
  60. await vi.waitFor(() => { expect(sent).toHaveLength(2) })
  61. expect(sent[1]?.url).toBe(`http://localhost:3000${mapUrl}`)
  62. const map = '{"version":3,"sections":[]}'
  63. deliver({
  64. t: 'res',
  65. id: 2,
  66. status: 200,
  67. headers: { 'content-type': 'application/json' },
  68. body: new TextEncoder().encode(map).buffer,
  69. })
  70. await loading
  71. const source = await blobs[0]?.text()
  72. const encoded = /sourceMappingURL=data:application\/json;charset=utf-8;base64,([^\s]+)/.exec(source ?? '')?.[1]
  73. if (encoded === undefined) throw new Error('localized bundle has no inline source map')
  74. const decoded = Uint8Array.from(atob(encoded), char => char.charCodeAt(0))
  75. expect(new TextDecoder().decode(decoded)).toBe(map)
  76. expect(revoked).toEqual(['blob:fixture-1'])
  77. })