|
| 1 | +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; |
| 2 | +import { getRemoteEntry, getRemoteInfo } from '../src/utils/load'; |
| 3 | +import { ModuleFederation } from '../src/core'; |
| 4 | +import { resetFederationGlobalInfo } from '../src/global'; |
| 5 | +import { RUNTIME_001, RUNTIME_008 } from '@module-federation/error-codes'; |
| 6 | +import { mockStaticServer, removeScriptTags } from './mock/utils'; |
| 7 | + |
| 8 | +// All fixture URLs are served via two complementary mechanisms both pointing to __tests__/: |
| 9 | +// 1. mockScriptDomResponse (setup.ts) — patches Element.prototype.appendChild, executes |
| 10 | +// matching JS files inline, fires element.onload without a real network request. |
| 11 | +// 2. mockStaticServer (below) — mocks window.fetch so jsdom's background script-fetch |
| 12 | +// also gets a valid response instead of failing with ECONNREFUSED. |
| 13 | +const BASE = 'http://localhost:1111/resources/load'; |
| 14 | + |
| 15 | +mockStaticServer({ |
| 16 | + baseDir: __dirname, |
| 17 | + filterKeywords: [], |
| 18 | + basename: 'http://localhost:1111/', |
| 19 | +}); |
| 20 | + |
| 21 | +const createMF = () => new ModuleFederation({ name: 'test-host', remotes: [] }); |
| 22 | + |
| 23 | +describe('getRemoteEntry - script load error discrimination', () => { |
| 24 | + beforeEach(() => { |
| 25 | + resetFederationGlobalInfo(); |
| 26 | + delete (globalThis as any)['remote']; |
| 27 | + removeScriptTags(); |
| 28 | + }); |
| 29 | + |
| 30 | + afterEach(() => { |
| 31 | + delete (globalThis as any)['remote']; |
| 32 | + removeScriptTags(); |
| 33 | + }); |
| 34 | + |
| 35 | + it('script load failure is reported as RUNTIME_008 with the original error included', async () => { |
| 36 | + // "missing.js" does not exist on disk. The mockScriptDomResponse interceptor tries |
| 37 | + // to fs.readFileSync it, throws ENOENT, which propagates synchronously through |
| 38 | + // document.head.appendChild → loadScript's Promise executor → promise rejects. |
| 39 | + // The onRejected handler in loadEntryScript wraps it as RUNTIME_008. |
| 40 | + const entry = `${BASE}/missing.js`; |
| 41 | + const origin = createMF(); |
| 42 | + const remoteInfo = getRemoteInfo({ name: 'remote', entry }); |
| 43 | + |
| 44 | + const err = await getRemoteEntry({ origin, remoteInfo }).catch((e) => e); |
| 45 | + |
| 46 | + expect(err.message).toContain(RUNTIME_008); |
| 47 | + // Original ENOENT message is forwarded into the RUNTIME_008 error |
| 48 | + expect(err.message).toMatch(/missing\.js|ENOENT/); |
| 49 | + }); |
| 50 | + |
| 51 | + it('IIFE execution error is reported as RUNTIME_008 with ScriptExecutionError details', async () => { |
| 52 | + // exec-error.js dispatches a window ErrorEvent with its own URL as filename. |
| 53 | + // dom.ts's executionErrorHandler captures it; when onload fires afterwards, |
| 54 | + // onErrorCallback(ScriptExecutionError) is called → loadScript rejects. |
| 55 | + const entry = `${BASE}/exec-error.js`; |
| 56 | + const origin = createMF(); |
| 57 | + const remoteInfo = getRemoteInfo({ name: 'remote', entry }); |
| 58 | + |
| 59 | + const err = await getRemoteEntry({ origin, remoteInfo }).catch((e) => e); |
| 60 | + |
| 61 | + expect(err.message).toContain(RUNTIME_008); |
| 62 | + expect(err.message).toContain('ScriptExecutionError'); |
| 63 | + expect(err.message).toContain('TypeError: exec failed'); |
| 64 | + }); |
| 65 | + |
| 66 | + it('script loaded successfully but global not registered throws RUNTIME_001, not RUNTIME_008', async () => { |
| 67 | + // no-global.js executes without side effects — global is never registered. |
| 68 | + // loadScript resolves (onload fires), handleRemoteEntryLoaded finds no global → RUNTIME_001. |
| 69 | + // The key assertion: RUNTIME_001 is NOT swallowed and replaced with RUNTIME_008. |
| 70 | + const entry = `${BASE}/no-global.js`; |
| 71 | + const origin = createMF(); |
| 72 | + const remoteInfo = getRemoteInfo({ name: 'remote', entry }); |
| 73 | + |
| 74 | + const err = await getRemoteEntry({ origin, remoteInfo }).catch((e) => e); |
| 75 | + |
| 76 | + expect(err.message).toContain(RUNTIME_001); |
| 77 | + expect(err.message).not.toContain(RUNTIME_008); |
| 78 | + }); |
| 79 | + |
| 80 | + it('script loaded and global registered returns the remote entry exports', async () => { |
| 81 | + // success.js sets globalThis['remote'] = { get, init } before onload fires. |
| 82 | + const entry = `${BASE}/success.js`; |
| 83 | + const origin = createMF(); |
| 84 | + const remoteInfo = getRemoteInfo({ name: 'remote', entry }); |
| 85 | + |
| 86 | + const result = await getRemoteEntry({ origin, remoteInfo }); |
| 87 | + |
| 88 | + expect(result).toEqual( |
| 89 | + expect.objectContaining({ |
| 90 | + get: expect.any(Function), |
| 91 | + init: expect.any(Function), |
| 92 | + }), |
| 93 | + ); |
| 94 | + }); |
| 95 | +}); |
0 commit comments