Testing with TypeScript and Node.js
TypeScript tests use the Embedded engine’s server form over the REST
API. The example needs nothing from npm: Node’s built-in test runner, the
global fetch and FormData, and Node’s native TypeScript type stripping
(Node 23.6 or later).
The runnable project is testing/typescript in the
examples download.
Start the engine
Section titled “Start the engine”From the container image:
export TINYCONDUCTOR_TOKEN=test-tokendocker run --rm -d --name tinyconductor-test -p 127.0.0.1:8080:8080 \ -e CONDUCTOR_AUTH_CREDENTIALS_JSON='[{"token":"test-token","subject":"tests","tenantId":"default","kind":"service","actions":["*"]}]' \ registry.tinyfactory.ai/tinyblox/tinyconductor:0.1.0 --mode embeddedOr from the release archive for your platform (downloads are listed in Embedded mode); on loopback it needs no credential:
./tinyconductor-0.1.0-linux-x86_64/tinyconductor --mode embeddedThe test reads TINYCONDUCTOR_URL (default http://127.0.0.1:8080) and the
optional TINYCONDUCTOR_TOKEN. The REST calls are listed in the
Java guide.
The test
Section titled “The test”import { readFile } from 'node:fs/promises';import { beforeEach, test } from 'node:test';import assert from 'node:assert/strict';
const BASE_URL = (process.env.TINYCONDUCTOR_URL ?? 'http://127.0.0.1:8080').replace(/\/$/, '');const TOKEN = process.env.TINYCONDUCTOR_TOKEN;const EXAMPLES = new URL('../../', import.meta.url); // the examples download's top directory
function headers(extra: Record<string, string> = {}): Record<string, string> { return TOKEN ? { ...extra, authorization: `Bearer ${TOKEN}` } : extra;}
async function call(method: string, path: string, body?: unknown): Promise<any> { const response = await fetch(BASE_URL + path, { method, headers: headers(body === undefined ? {} : { 'content-type': 'application/json' }), body: body === undefined ? undefined : JSON.stringify(body), }); const text = await response.text(); assert.ok(response.ok, `${method} ${path} answered ${response.status}: ${text}`); return text ? JSON.parse(text) : undefined;}
async function deploy(fileName: string, contentType: string): Promise<void> { const form = new FormData(); const bytes = await readFile(new URL(fileName, EXAMPLES)); form.append('resource', new Blob([bytes], { type: contentType }), fileName); const response = await fetch(`${BASE_URL}/v1/deployments`, { method: 'POST', headers: headers(), body: form }); assert.ok(response.ok, `deploy ${fileName} answered ${response.status}: ${await response.text()}`);}
async function completeJob(type: string, variables: Record<string, unknown>): Promise<void> { const { jobs } = await call('POST', '/v1/jobs/activation', { type, timeout: 30000, maxJobsToActivate: 1 }); assert.equal(jobs.length, 1, `expected one ${type} job`); await call('POST', `/v1/jobs/${jobs[0].jobKey}/completion`, { leaseEpoch: jobs[0].leaseEpoch, variables });}
beforeEach(async () => { // Every test starts from an empty engine at the fixed epoch. await call('POST', '/v1/embedded/reset'); await deploy('order.form', 'application/json'); await deploy('order-decisions.dmn', 'application/xml'); await deploy('order.bpmn', 'application/xml');});
test('order completes after the review period', async () => { const { processInstanceKey: key } = await call('POST', '/v1/process-instances', { processDefinitionId: 'order-process', variables: { orderId: 4711, amount: 99.9 }, }); // keys are JSON strings
await completeJob('charge-card', { charged: true }); // The 24-hour review timer fires inside this call; no sleeping or polling. await call('POST', '/v1/embedded/clock/advance-by', { milliseconds: 86_400_000 }); await completeJob('send-invoice', {});
const { items } = await call('POST', '/v1/user-tasks/search', { processInstanceKey: key }); assert.deepEqual(items.map((t: any) => t.elementId), ['confirm-shipment']); await call('POST', `/v1/user-tasks/${items[0].userTaskKey}/completion`, { variables: { shipped: true } });
const { records } = await call('GET', '/v1/embedded/records'); const completed = new Set( records .filter((r: any) => r.valueType === 'PROCESS_INSTANCE' && r.intent === 'ELEMENT_COMPLETED' && r.value.processInstanceKey === key) .map((r: any) => r.value.elementId), ); for (const element of ['order-process', 'charge-card', 'send-invoice', 'confirm-shipment']) { assert.ok(completed.has(element), `${element} completed`); }});Run it:
cd tinyconductor-0.1.0-examples/testing/typescriptnpm test # node --testKeys stay strings end to end, so nothing is lost to JavaScript’s 2^53
number limit. Tests that share one engine must not run in parallel, since
each begins with a reset; node --test runs the tests of one file in order.