| const fs = require('node:fs'); |
| const path = require('node:path'); |
| const vm = require('node:vm'); |
| const { execFileSync } = require('node:child_process'); |
| const { pathToFileURL } = require('node:url'); |
|
|
| const { createDom } = require('../../support/fake-dom'); |
|
|
| const ROOT = path.resolve(__dirname, '../../..'); |
|
|
| |
| |
| function resolveSolverForgeUiRuntime() { |
| const metadata = JSON.parse(execFileSync('cargo', ['metadata', '--format-version', '1'], { |
| cwd: ROOT, |
| encoding: 'utf8', |
| })); |
| const uiPackage = metadata.packages.find((pkg) => pkg.name === 'solverforge-ui'); |
| if (!uiPackage) { |
| throw new Error('cargo metadata did not include solverforge-ui'); |
| } |
| return path.resolve(path.dirname(uiPackage.manifest_path), 'static/sf/sf.js'); |
| } |
|
|
| const SF_RUNTIME_PATH = resolveSolverForgeUiRuntime(); |
| const SF_SOURCE = fs.readFileSync(SF_RUNTIME_PATH, 'utf8'); |
|
|
| |
| function createJsonResponse(value) { |
| return { |
| ok: true, |
| status: 200, |
| statusText: 'OK', |
| headers: { |
| get(name) { |
| return String(name).toLowerCase() === 'content-type' ? 'application/json' : null; |
| }, |
| }, |
| json: async () => value, |
| text: async () => JSON.stringify(value), |
| }; |
| } |
|
|
| |
| function loadSfRuntime(window, document, Node) { |
| const wrapped = `(function(window, document, Node) { ${SF_SOURCE}\n return window.SF; })`; |
| const factory = vm.runInThisContext(wrapped, { |
| filename: SF_RUNTIME_PATH, |
| }); |
| return factory(window, document, Node); |
| } |
|
|
| |
| function saveGlobal(name) { |
| return { |
| name, |
| existed: Object.prototype.hasOwnProperty.call(globalThis, name), |
| value: globalThis[name], |
| }; |
| } |
|
|
| |
| function restoreGlobals(saved) { |
| saved.forEach((entry) => { |
| if (entry.existed) globalThis[entry.name] = entry.value; |
| else delete globalThis[entry.name]; |
| }); |
| } |
|
|
| |
| function uniqueModuleUrl(relativePath) { |
| const url = new URL(pathToFileURL(path.resolve(ROOT, relativePath)).href); |
| url.searchParams.set('t', `${Date.now()}-${Math.random()}`); |
| return url.href; |
| } |
|
|
| |
| async function importModule(relativePath) { |
| return import(uniqueModuleUrl(relativePath)); |
| } |
|
|
| |
| async function withBrowserEnv(options, run) { |
| const settings = options || {}; |
| const { document, window, Node } = createDom(); |
| const errors = []; |
| const appRoot = settings.appRoot === false ? null : document.createElement('div'); |
| if (appRoot) { |
| appRoot.id = 'sf-app'; |
| document.body.appendChild(appRoot); |
| } |
|
|
| const fauxConsole = { |
| log: console.log, |
| warn: console.warn, |
| info: console.info, |
| error(...args) { |
| errors.push(args.map((arg) => String(arg)).join(' ')); |
| }, |
| }; |
| const fetchStub = settings.fetch || (async function unexpectedFetch(url) { |
| throw new Error(`unexpected fetch ${url}`); |
| }); |
| const eventSourceStub = settings.EventSource || function EventSource() { |
| throw new Error('EventSource should not be used in this test'); |
| }; |
| const location = settings.location || { origin: 'http://localhost:7861' }; |
|
|
| window.window = window; |
| window.document = document; |
| window.Node = Node; |
| window.console = fauxConsole; |
| window.fetch = fetchStub; |
| window.EventSource = eventSourceStub; |
| window.location = location; |
|
|
| const saved = ['window', 'document', 'Node', 'fetch', 'EventSource', 'location', 'SF', 'console'] |
| .map(saveGlobal); |
| globalThis.window = window; |
| globalThis.document = document; |
| globalThis.Node = Node; |
| globalThis.fetch = fetchStub; |
| globalThis.EventSource = eventSourceStub; |
| globalThis.location = location; |
| globalThis.console = fauxConsole; |
| globalThis.SF = loadSfRuntime(window, document, Node); |
| window.SF = globalThis.SF; |
|
|
| try { |
| return await run({ |
| ROOT, |
| document, |
| window, |
| Node, |
| appRoot, |
| errors, |
| importModule, |
| createJsonResponse, |
| }); |
| } finally { |
| restoreGlobals(saved); |
| } |
| } |
|
|
| |
| async function loadFullApp(options = {}) { |
| const sfConfig = options.sfConfig || JSON.parse( |
| fs.readFileSync(path.resolve(ROOT, 'static/sf-config.json'), 'utf8'), |
| ); |
| const uiModel = options.uiModel || JSON.parse( |
| fs.readFileSync(path.resolve(ROOT, 'static/generated/ui-model.json'), 'utf8'), |
| ); |
| const demoData = options.demoData || { employees: [], shifts: [] }; |
|
|
| return withBrowserEnv({ |
| fetch: async (url) => { |
| if (url === '/sf-config.json') return createJsonResponse(sfConfig); |
| if (url === '/generated/ui-model.json') return createJsonResponse(uiModel); |
| if (url === '/demo-data') return createJsonResponse([String(sfConfig.defaultDemoId || 'LARGE')]); |
| if (url === `/demo-data/${String(sfConfig.defaultDemoId || 'LARGE')}`) return createJsonResponse(demoData); |
| throw new Error(`unexpected fetch ${url}`); |
| }, |
| }, async ({ document, errors, importModule, window }) => { |
| const { bootApp } = await importModule('static/app/main.mjs'); |
| await bootApp(window); |
| await new Promise((resolve) => setTimeout(resolve, 0)); |
| return { document, errors }; |
| }); |
| } |
|
|
| module.exports = { |
| ROOT, |
| createJsonResponse, |
| importModule, |
| loadFullApp, |
| withBrowserEnv, |
| }; |
|
|