solverforge-ui 0.4.1

Frontend component library for SolverForge constraint-optimization applications
Documentation
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');
});