Skip to content

Commit d581b53

Browse files
committed
WIP
1 parent 4c8234f commit d581b53

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1791
-1294
lines changed

README.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -60,31 +60,31 @@ We recommend using [Camunda 8 Run](https://docs.camunda.io/docs/self-managed/qui
6060

6161
You can configure your Camunda 8 environment in the `demo/.env` file.
6262

63-
## Test Scenarios
63+
## Integration Fixtures
6464

65-
The integration tests in `test/` are driven by **recorded fixture files** (`test/fixtures/scenarios/*.json`) rather than a live Camunda instance. Each fixture captures a full timeline of API responses from a real run, which is then replayed deterministically in CI.
65+
The integration tests in `test/` are driven by **recorded fixture files** (`test/fixtures/integration/*.json`) rather than a live Camunda instance. Each fixture captures a full timeline of API responses from a real run, which is then replayed deterministically in CI.
6666

6767
### How it works
6868

69-
1. A *scenario* is a self-contained folder under `test/scenarios/` with its own BPMN diagram (`process.bpmn`) and a definition file (`scenario.mjs`).
70-
2. The headless runner (`test/scenarios/run.mjs`) deploys the BPMN to a live Camunda 8 instance, starts a process instance, activates any job workers, polls until the process reaches a terminal state, and writes the recorded timeline to `test/fixtures/scenarios/<name>.json`.
69+
1. A *recording* is a self-contained folder under `test/recording/` with its own BPMN diagram (`process.bpmn`) and a definition file (`recording.mjs`).
70+
2. The headless runner (`test/recording/run.mjs`) deploys the BPMN to a live Camunda 8 instance, starts a process instance, activates any job workers, polls until the process reaches a terminal state, and writes the recorded timeline to `test/fixtures/integration/<name>.json`.
7171
3. The committed JSON fixtures are what the unit tests actually execute against — no live Camunda required in CI.
7272
4. Re-running the recorder and doing a `git diff` is an effective regression test against the Camunda engine: if the fixture changes unexpectedly, something in engine behaviour has changed.
7373

7474
### Folder structure
7575

7676
```
77-
test/scenarios/
77+
test/recording/
7878
helpers.mjs # worker factory helpers (completeWorker, failWorker, …)
7979
run.mjs # headless recorder CLI
8080
service-task-success/
81-
process.bpmn # owned BPMN — do not share across scenarios
82-
scenario.mjs # scenario definition
81+
process.bpmn # owned BPMN — do not share across recordings
82+
recording.mjs # recording definition
8383
service-task-incident/
8484
...
8585
```
8686

87-
Each `scenario.mjs` exports a default object:
87+
Each `recording.mjs` exports a default object:
8888

8989
```js
9090
export default {
@@ -113,24 +113,24 @@ Worker helpers (`helpers.mjs`):
113113

114114
### Recording fixtures
115115

116-
Requires a live Camunda 8 instance configured in `demo/.env` (same as `npm start`).
116+
Requires a live Camunda 8 instance configured in `.env` (same as `npm start`).
117117

118118
```sh
119-
# Record all scenarios
120-
npm run scenarios:record
119+
# Record all fixtures
120+
npm run fixtures:record
121121

122-
# Record a single scenario
123-
npm run scenarios:record:single test/scenarios/service-task-success/scenario.mjs
122+
# Record a single fixture
123+
npm run fixtures:record:single test/recording/service-task-success/recording.mjs
124124

125125
# Review what changed
126-
npm run scenarios:diff
126+
npm run fixtures:diff
127127
```
128128

129-
### Adding a new scenario
129+
### Adding a new recording
130130

131-
1. Create a new folder under `test/scenarios/` with a minimal `process.bpmn` and a `scenario.mjs` that exports the scenario definition. Use a unique `processId` and a scoped job type (e.g. `task-testing:<scenario-name>`) to avoid collisions on a shared cluster.
132-
2. Run `npm run scenarios:record:single test/scenarios/<name>/scenario.mjs` to produce the fixture JSON.
133-
3. Write a spec in `test/` that uses `replayScenario` / `createReplayApi` to exercise the component with the new fixture.
131+
1. Create a new folder under `test/recording/` with a minimal `process.bpmn` and a `recording.mjs` that exports the recording definition. Use a unique `processId` and a scoped job type (e.g. `task-testing:<name>`) to avoid collisions on a shared cluster.
132+
2. Run `npm run fixtures:record:single test/recording/<name>/recording.mjs` to produce the fixture JSON.
133+
3. Write a spec in `test/` that uses `replayFixture` / `createReplayApi` to exercise the component with the new fixture.
134134
4. Commit both the `process.bpmn` and the generated JSON fixture.
135135

136136
## Build

demo/.env

Lines changed: 0 additions & 8 deletions
This file was deleted.

demo/RecorderBar.jsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export default function RecorderBar({ injector }) {
140140

141141
return (
142142
<div className="recorder-bar">
143-
<span className="recorder-bar__icon">{ recording ? '' : '📼' }</span>
143+
<span className={ `recorder-bar__indicator${ recording ? ' recorder-bar__indicator--recording' : '' }` } />
144144

145145
{ !recording ? (
146146
<div className="recorder-bar__form">
@@ -169,13 +169,6 @@ export default function RecorderBar({ injector }) {
169169
value={ elementId }
170170
onChange={ e => setElementId(e.target.value) }
171171
/>
172-
<input
173-
className="recorder-bar__input recorder-bar__input--narrow"
174-
type="text"
175-
placeholder="Process ID"
176-
value={ processId }
177-
readOnly
178-
/>
179172
<button className="recorder-bar__button recorder-bar__button--start" onClick={ start }>
180173
Start recording
181174
</button>

demo/api.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Shared Camunda REST API wrapper.
33
*
44
* createApi(client) returns an object whose methods mirror the SDK calls used
5-
* by both demo/server.mjs and test/scenarios/run.mjs. Every method returns a
5+
* by both demo/server.mjs and test/recording/run.mjs. Every method returns a
66
* `{ success: true, response }` or `{ success: false, error }` object so
77
* callers never need their own try/catch. Recording stays with the caller.
88
*

demo/recorder.mjs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* ApiRecorder — captures API responses during a task execution session
3-
* so they can be saved as test fixture scenarios for replay-based testing.
3+
* so they can be saved as integration fixture JSON files for replay-based testing.
44
*
55
* Usage:
66
* ```js
@@ -13,7 +13,7 @@
1313
* recorder.record('poll', { jobsResult, userTasksResult, ... });
1414
*
1515
* // When execution finishes:
16-
* recorder.save(); // writes to test/fixtures/scenarios/<name>.json
16+
* recorder.save(); // writes to test/fixtures/integration/<name>.json
1717
* ```
1818
*/
1919

@@ -24,19 +24,19 @@ import { fileURLToPath } from 'url';
2424
const __filename = fileURLToPath(import.meta.url);
2525
const __dirname = path.dirname(__filename);
2626

27-
const SCENARIOS_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'scenarios');
27+
const INTEGRATION_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'integration');
2828

2929
export default class ApiRecorder {
3030

3131
/**
3232
* @param {Object} [options]
33-
* @param {string} [options.outputDir] — directory to write scenario JSON files.
34-
* Defaults to `test/fixtures/scenarios/` relative to the project root.
33+
* @param {string} [options.outputDir] — directory to write integration fixture JSON files.
34+
* Defaults to `test/fixtures/integration/` relative to the project root.
3535
*/
3636
constructor({ outputDir } = {}) {
3737

3838
/** @type {string} */
39-
this._outputDir = outputDir || SCENARIOS_DIR;
39+
this._outputDir = outputDir || INTEGRATION_DIR;
4040

4141
/** @type {string|null} */
4242
this._name = null;

demo/server.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ let api;
2121

2222
try {
2323

24-
const config = dotenv.config({ path: path.join(__dirname, '.env') }).parsed;
24+
const config = dotenv.config({ path: path.join(__dirname, '..', '.env') }).parsed;
2525

2626
if (!Object.keys(config)?.length) {
2727
throw new Error('No configuration found in .env file');

demo/style.css

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,22 @@ body {
6161
background: hsl(225, 10%, 95%);
6262
border-bottom: 1px solid hsl(225, 10%, 75%);
6363
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
64-
font-size: 13px;
64+
font-size: 14px;
6565
flex-shrink: 0;
6666
}
6767

68-
.recorder-bar__icon {
69-
font-size: 16px;
68+
.recorder-bar__indicator {
69+
width: 10px;
70+
height: 10px;
71+
border-radius: 50%;
72+
background: #0f62fe;
7073
flex-shrink: 0;
7174
}
7275

76+
.recorder-bar__indicator--recording {
77+
background: #da1e28;
78+
}
79+
7380
.recorder-bar__form {
7481
display: flex;
7582
align-items: center;
@@ -107,7 +114,6 @@ body {
107114
.recorder-bar__input {
108115
padding: 3px 8px;
109116
border: 1px solid hsl(225, 10%, 75%);
110-
border-radius: 3px;
111117
font-size: 12px;
112118
font-family: inherit;
113119
min-width: 120px;
@@ -131,7 +137,6 @@ body {
131137
.recorder-bar__button {
132138
padding: 3px 10px;
133139
border: none;
134-
border-radius: 3px;
135140
font-size: 12px;
136141
font-family: inherit;
137142
cursor: pointer;
@@ -144,21 +149,21 @@ body {
144149
}
145150

146151
.recorder-bar__button--start:hover {
147-
background: #0043ce;
152+
background: #0050e6;
148153
}
149154

150155
.recorder-bar__button--save {
151-
background: #198038;
156+
background: #0f62fe;
152157
color: white;
153158
}
154159

155160
.recorder-bar__button--save:hover {
156-
background: #0e6027;
161+
background: #0050e6;
157162
}
158163

159164
.recorder-bar__button--discard {
160-
background: hsl(225, 10%, 80%);
161-
color: hsl(225, 10%, 20%);
165+
background: #da1e28;
166+
color: white;
162167
}
163168

164169
.recorder-bar__button--discard:hover {

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
"lint": "eslint .",
2020
"types": "tsc --noEmit",
2121
"prepare": "npm run build",
22-
"scenarios:record": "node test/scenarios/run.mjs --all",
23-
"scenarios:record:single": "node test/scenarios/run.mjs",
24-
"scenarios:diff": "git diff --stat test/fixtures/scenarios/"
22+
"fixtures:record": "node test/recording/run.mjs --all",
23+
"fixtures:record:single": "node test/recording/run.mjs",
24+
"fixtures:diff": "git diff --stat test/fixtures/integration/"
2525
},
2626
"license": "MIT",
2727
"dependencies": {
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { replayFixture } from './util/replayFixture';
2+
3+
import serviceTaskSuccess from './fixtures/integration/service-task-success.json';
4+
import serviceTaskIncident from './fixtures/integration/service-task-incident.json';
5+
import subprocessWithChildren from './fixtures/integration/subprocess-with-children.json';
6+
import eventSubprocess from './fixtures/integration/event-subprocess.json';
7+
import userTaskWaiting from './fixtures/integration/user-task-waiting.json';
8+
import boundaryEventTriggered from './fixtures/integration/boundary-event-triggered.json';
9+
import scriptTask from './fixtures/integration/script-task.json';
10+
11+
12+
/**
13+
* Fixture-based smoke tests — verify that real Camunda API response shapes
14+
* are compatible with ExecutionLog without throwing.
15+
*
16+
* Behavioral assertions (upsert, state transitions, display filtering, child
17+
* scoping) live in ExecutionLog.spec.js using controlled data.
18+
*/
19+
describe('ExecutionLog - integration', function() {
20+
21+
const allFixtures = [
22+
{ name: 'service-task-success', fixture: serviceTaskSuccess },
23+
{ name: 'service-task-incident', fixture: serviceTaskIncident },
24+
{ name: 'subprocess-with-children', fixture: subprocessWithChildren },
25+
{ name: 'event-subprocess', fixture: eventSubprocess },
26+
{ name: 'user-task-waiting', fixture: userTaskWaiting },
27+
{ name: 'boundary-event-triggered', fixture: boundaryEventTriggered },
28+
{ name: 'script-task', fixture: scriptTask }
29+
];
30+
31+
32+
describe('all fixtures', function() {
33+
34+
for (const { name, fixture } of allFixtures) {
35+
36+
it(`should replay ${ name } without throwing`, function() {
37+
const { log } = replayFixture(fixture);
38+
39+
expect(log.getEntries()).to.be.an('array');
40+
expect(log.getDisplayEntries()).to.be.an('array');
41+
expect(log.isFinished()).to.be.a('boolean');
42+
});
43+
}
44+
});
45+
46+
47+
describe('field shape compatibility', function() {
48+
49+
it('should read job fields from real API response (service-task-success)', function() {
50+
const { log } = replayFixture(serviceTaskSuccess);
51+
52+
const job = log.getEntries().find(e => e.type === 'job');
53+
expect(job).to.exist;
54+
expect(job.data).to.have.property('jobKey');
55+
expect(job.data).to.have.property('type');
56+
expect(job.data).to.have.property('processInstanceKey');
57+
expect(job.data).to.have.property('elementId');
58+
expect(job.data).to.have.property('elementInstanceKey');
59+
expect(job.data).to.have.property('state');
60+
expect(job.data).to.have.property('retries');
61+
expect(job.data).to.have.property('creationTime');
62+
expect(job.data).to.have.property('worker');
63+
expect(job.data).to.have.property('customHeaders');
64+
});
65+
66+
it('should read error fields from real API response (service-task-incident)', function() {
67+
const { log } = replayFixture(serviceTaskIncident);
68+
69+
const job = log.getEntries().find(e => e.type === 'job');
70+
expect(job).to.exist;
71+
expect(job.data).to.have.property('errorMessage');
72+
expect(job.data).to.have.property('errorCode');
73+
expect(job.data.retries).to.equal(0);
74+
});
75+
76+
it('should read element instance fields from real API response (subprocess-with-children)', function() {
77+
const { log } = replayFixture(subprocessWithChildren);
78+
79+
const entry = log.getEntries().find(e => e.type === 'element-instance');
80+
expect(entry).to.exist;
81+
expect(entry.data).to.have.property('elementInstanceKey');
82+
expect(entry.data).to.have.property('processInstanceKey');
83+
expect(entry.data).to.have.property('elementId');
84+
expect(entry.data).to.have.property('type');
85+
expect(entry.data).to.have.property('state');
86+
expect(entry.data).to.have.property('startDate');
87+
});
88+
89+
it('should read user task fields from real API response (user-task-waiting)', function() {
90+
const { log } = replayFixture(userTaskWaiting);
91+
92+
const userTask = log.getEntries().find(e => e.type === 'user-task');
93+
expect(userTask).to.exist;
94+
expect(userTask.data).to.have.property('userTaskKey');
95+
expect(userTask.data).to.have.property('state');
96+
expect(userTask.data).to.have.property('assignee');
97+
expect(userTask.data).to.have.property('candidateGroups');
98+
expect(userTask.data).to.have.property('candidateUsers');
99+
expect(userTask.data).to.have.property('elementId');
100+
expect(userTask.data).to.have.property('creationDate');
101+
expect(userTask.data).to.have.property('priority');
102+
});
103+
104+
describe('script-task', function() {
105+
106+
it('should be finished', function() {
107+
const { log } = replayFixture(scriptTask);
108+
expect(log.isFinished()).to.be.true;
109+
});
110+
111+
it('should produce no job entries — script tasks complete without a worker', function() {
112+
const { log } = replayFixture(scriptTask);
113+
expect(log.getEntries().filter(e => e.type === 'job')).to.have.length(0);
114+
});
115+
116+
it('should read element instance fields from real API response', function() {
117+
118+
// replay without elementId so ScriptTask_1 is NOT filtered out
119+
const { log } = replayFixture(scriptTask, { elementId: '__none__' });
120+
const entry = log.getEntries().find(e => e.type === 'element-instance');
121+
expect(entry).to.exist;
122+
expect(entry.data).to.have.property('elementInstanceKey');
123+
expect(entry.data).to.have.property('processInstanceKey');
124+
expect(entry.data).to.have.property('elementId');
125+
expect(entry.data).to.have.property('type');
126+
expect(entry.data).to.have.property('state');
127+
expect(entry.data).to.have.property('startDate');
128+
expect(entry.data).to.have.property('endDate');
129+
expect(entry.data.state).to.equal('COMPLETED');
130+
expect(entry.data.type).to.equal('SCRIPT_TASK');
131+
});
132+
});
133+
});
134+
});
135+

0 commit comments

Comments
 (0)