Skip to content

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.

From the container image:

Terminal window
export TINYCONDUCTOR_TOKEN=test-token
docker 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 embedded

Or from the release archive for your platform (downloads are listed in Embedded mode); on loopback it needs no credential:

Terminal window
./tinyconductor-0.1.0-linux-x86_64/tinyconductor --mode embedded

The 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.

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:

Terminal window
cd tinyconductor-0.1.0-examples/testing/typescript
npm test # node --test

Keys 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.