const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');
const { createDom } = require('./support/fake-dom');
const ROOT = path.resolve(__dirname, '..');
function loadSf(files, overrides = {}) {
const { document, window, Node } = createDom();
const context = vm.createContext({
console,
document,
window,
Node,
Promise,
setTimeout,
clearTimeout,
...overrides,
});
files.forEach((file) => {
const source = fs.readFileSync(path.join(ROOT, file), 'utf8');
vm.runInContext(source, context, { filename: file });
});
return { SF: context.window.SF, document };
}
test('tauri createSchedule normalizes object and numeric ids to strings', async () => {
const calls = [];
const { SF } = loadSf(['js-src/00-core.js', 'js-src/10-backend.js'], {
fetch() {
throw new Error('unexpected fetch');
},
});
const backendWithObject = SF.createBackend({
type: 'tauri',
invoke(command, payload) {
calls.push({ command, payload });
return Promise.resolve({ jobId: 42 });
},
listen() {
return Promise.resolve(function () {});
},
});
assert.equal(await backendWithObject.createSchedule({ foo: 'bar' }), '42');
assert.equal(calls[0].command, 'create_schedule');
const backendWithNumber = SF.createBackend({
type: 'tauri',
invoke() {
return Promise.resolve(7);
},
listen() {
return Promise.resolve(function () {});
},
});
assert.equal(await backendWithNumber.createSchedule({}), '7');
});
test('tauri stopSchedule uses the stop command while deleteSchedule stays separately addressable', async () => {
const calls = [];
const { SF } = loadSf(['js-src/00-core.js', 'js-src/10-backend.js']);
const backend = SF.createBackend({
type: 'tauri',
invoke(command, payload) {
calls.push({ command, payload });
return Promise.resolve(null);
},
listen() {
return Promise.resolve(function () {});
},
});
await backend.stopSchedule('job-3');
await backend.deleteSchedule('job-3');
assert.equal(calls.length, 2);
assert.equal(calls[0].command, 'stop_solve');
assert.equal(calls[0].payload.id, 'job-3');
assert.equal(calls[1].command, 'delete_schedule');
assert.equal(calls[1].payload.id, 'job-3');
});
test('non-tauri backend labels still use the generic HTTP adapter', async () => {
const fetchCalls = [];
const { SF } = loadSf(['js-src/00-core.js', 'js-src/10-backend.js'], {
fetch(url, opts) {
fetchCalls.push({ url, opts });
return Promise.resolve({
ok: true,
headers: { get() { return 'application/json'; } },
json() { return Promise.resolve({ id: 'job-9' }); },
});
},
});
const backend = SF.createBackend({
type: 'rails',
baseUrl: '/api',
schedulesPath: '/jobs',
});
assert.equal(await backend.createSchedule({ foo: 'bar' }), 'job-9');
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, '/api/jobs');
assert.equal(fetchCalls[0].opts.method, 'POST');
});
test('tauri streamEvents keeps id-less typed updates and filters mismatched job ids', async () => {
let handler = null;
const received = [];
const { SF } = loadSf(['js-src/00-core.js', 'js-src/10-backend.js']);
const backend = SF.createBackend({
type: 'tauri',
invoke() {
return Promise.resolve('job-1');
},
listen(_eventName, onEvent) {
handler = onEvent;
return Promise.resolve(function () {});
},
});
backend.streamEvents('job-1', function (payload) {
received.push(payload);
});
await Promise.resolve();
handler({ payload: { eventType: 'progress', currentScore: '0hard/0soft', bestScore: '0hard/0soft', movesPerSecond: 12 } });
handler({ payload: { data: { id: 'job-1' }, eventType: 'best_solution', currentScore: '0hard/-1soft', bestScore: '0hard/-1soft', solution: { id: 'job-1', score: '0hard/-1soft' } } });
handler({ payload: { jobId: 'job-2', eventType: 'finished', currentScore: '0hard/0soft', bestScore: '0hard/0soft', solution: { id: 'job-2', score: '0hard/0soft' } } });
assert.equal(received.length, 2);
assert.equal(received[0].eventType, 'progress');
assert.equal(received[0].currentScore, '0hard/0soft');
assert.equal(received[1].data.id, 'job-1');
});