- Bundle robot3 1.3.0 locally (not yet published to npm) - Fix remaining type inference issues with `as any` casts for invoke() events - Resolves state machine transition type errors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
388 lines
11 KiB
JavaScript
388 lines
11 KiB
JavaScript
import { createMachine, immediate, interpret, invoke, reduce, state, state as final, transition } from '../machine.js';
|
|
|
|
QUnit.module('Invoke', hooks => {
|
|
QUnit.module('Promise');
|
|
|
|
QUnit.test('Goes to the "done" event when complete', async assert => {
|
|
let machine = createMachine({
|
|
one: state(transition('click', 'two')),
|
|
two: invoke(() => Promise.resolve(13),
|
|
transition('done', 'three',
|
|
reduce((ctx, ev) => ({ ...ctx, age: ev.data }))
|
|
)
|
|
),
|
|
three: state()
|
|
}, () => ({age: 0}));
|
|
|
|
let service = interpret(machine, () => {});
|
|
service.send('click');
|
|
await Promise.resolve();
|
|
assert.equal(service.context.age, 13, 'Invoked');
|
|
assert.equal(service.machine.current, 'three', 'now in the next state');
|
|
});
|
|
|
|
QUnit.test('Goes to the "error" event when there is an error', async assert => {
|
|
let machine = createMachine({
|
|
one: state(transition('click', 'two')),
|
|
two: invoke(() => Promise.reject(new Error('oh no')),
|
|
transition('error', 'three',
|
|
reduce((ctx, ev) => ({ ...ctx, error: ev.error }))
|
|
)
|
|
),
|
|
three: state()
|
|
}, () => ({age: 0}));
|
|
|
|
let service = interpret(machine, () => {});
|
|
service.send('click');
|
|
await Promise.resolve(); await Promise.resolve();
|
|
assert.equal(service.context.error.message, 'oh no', 'Got the right error');
|
|
});
|
|
|
|
QUnit.test('The initial state can be an invoke', async assert => {
|
|
let machine = createMachine({
|
|
one: invoke(() => Promise.resolve(2),
|
|
transition('done', 'two', reduce((ctx, ev) => ({...ctx, age: ev.data})))
|
|
),
|
|
two: state()
|
|
}, () => ({ age: 0 }));
|
|
|
|
let service = interpret(machine, () => {});
|
|
await Promise.resolve();
|
|
assert.equal(service.context.age, 2, 'Invoked immediately');
|
|
assert.equal(service.machine.current, 'two', 'in the new state');
|
|
});
|
|
|
|
QUnit.test('Should not fire "done" event when state changes', async assert => {
|
|
const wait = ms => () => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
let machine = createMachine({
|
|
one: state(transition('click', 'two')),
|
|
two: invoke(wait(10),
|
|
transition('done', 'one'),
|
|
transition('click', 'three')
|
|
),
|
|
three: state(
|
|
transition('done', 'error'),
|
|
),
|
|
error: state(),
|
|
});
|
|
|
|
let service = interpret(machine, () => { });
|
|
service.send('click');
|
|
service.send('click');
|
|
await wait(15)()
|
|
assert.equal(service.machine.current, 'three', 'now in the next state');
|
|
});
|
|
|
|
QUnit.test('Should fire "done" when context changes', async assert => {
|
|
const wait = ms => () => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
let machine = createMachine({
|
|
one: state(transition('click', 'two')),
|
|
two: invoke(wait(10),
|
|
transition('done', 'three'),
|
|
transition('click', 'two', reduce((ctx) => ({ value: ctx.value + 1 })))
|
|
),
|
|
three: state(),
|
|
error: state(),
|
|
}, () => ({ value: 0 }));
|
|
|
|
let service = interpret(machine, () => { });
|
|
service.send('click');
|
|
service.send('click');
|
|
service.send('click');
|
|
await wait(15)()
|
|
assert.equal(service.context.value, 2, 'value should be 2');
|
|
assert.equal(service.machine.current, 'three', 'now in the correct state');
|
|
});
|
|
|
|
QUnit.module('Machine');
|
|
|
|
QUnit.test('Can invoke a child machine', async assert => {
|
|
assert.expect(4);
|
|
let one = createMachine({
|
|
nestedOne: state(
|
|
transition('go', 'nestedTwo')
|
|
),
|
|
nestedTwo: final()
|
|
});
|
|
let two = createMachine({
|
|
one: state(
|
|
transition('go', 'two')
|
|
),
|
|
two: invoke(one,
|
|
transition('done', 'three')
|
|
),
|
|
three: final()
|
|
});
|
|
let c = 0;
|
|
let service = interpret(two, thisService => {
|
|
switch(c) {
|
|
case 0:
|
|
assert.equal(service.machine.current, 'two');
|
|
break;
|
|
case 1:
|
|
assert.notEqual(thisService, service, 'second time a different service');
|
|
break;
|
|
case 2:
|
|
assert.equal(service.machine.current, 'three', 'now in three state');
|
|
break;
|
|
}
|
|
c++;
|
|
});
|
|
service.send('go');
|
|
service.child.send('go');
|
|
assert.equal(c, 3, 'there were 3 transitions');
|
|
});
|
|
|
|
QUnit.test('Can invoke a dynamic child machine', async assert => {
|
|
assert.expect(10);
|
|
let dynamicMachines = [
|
|
createMachine({
|
|
nestedOne: state(
|
|
transition('go', 'nestedTwo')
|
|
),
|
|
nestedTwo: final()
|
|
}),
|
|
createMachine({
|
|
nestedThree: state(
|
|
transition('go', 'nestedFour')
|
|
),
|
|
nestedFour: final()
|
|
})
|
|
]
|
|
|
|
let root = createMachine({
|
|
one: state(
|
|
transition('go', 'two')
|
|
),
|
|
two: invoke(() => dynamicMachines[0],
|
|
transition('done', 'three')
|
|
),
|
|
three: state(
|
|
transition('go', 'four')
|
|
),
|
|
four: invoke(() => dynamicMachines[1],
|
|
transition('done', 'five')
|
|
),
|
|
five: final()
|
|
});
|
|
let c = 0;
|
|
let service = interpret(root, thisService => {
|
|
switch(c) {
|
|
case 0:
|
|
assert.equal(service.machine.current, 'two');
|
|
break;
|
|
case 1:
|
|
assert.notEqual(thisService, service, 'second time a different service');
|
|
assert.equal(thisService.machine.current, 'nestedTwo');
|
|
break;
|
|
case 2:
|
|
assert.equal(thisService, service, 'equal service');
|
|
assert.equal(service.machine.current, 'three', 'now in three state');
|
|
break;
|
|
case 3:
|
|
assert.equal(service.machine.current, 'four');
|
|
break;
|
|
case 4:
|
|
assert.notEqual(thisService, service, 'third time a different service');
|
|
assert.equal(thisService.machine.current, 'nestedFour');
|
|
break;
|
|
case 5:
|
|
assert.equal(service.machine.current, 'five', 'now in five state');
|
|
break;
|
|
}
|
|
c++;
|
|
});
|
|
service.send('go');
|
|
service.child.send('go');
|
|
service.send('go');
|
|
service.child.send('go');
|
|
assert.equal(c, 6, 'there were 6 transitions');
|
|
});
|
|
|
|
QUnit.test('Child machines receive events from their parents', async assert => {
|
|
const action = fn =>
|
|
reduce((ctx, ev) => {
|
|
fn(ctx, ev);
|
|
return ctx;
|
|
});
|
|
|
|
const wait = ms => () => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
const child = createMachine({
|
|
init: state(
|
|
immediate('waiting',
|
|
action(ctx => {
|
|
ctx.stuff.push(1);
|
|
})
|
|
)
|
|
),
|
|
waiting: invoke(
|
|
wait(50),
|
|
transition('done', 'fin',
|
|
action(ctx => {
|
|
ctx.stuff.push(2);
|
|
})
|
|
)
|
|
),
|
|
fin: state()
|
|
}, ctx => ctx);
|
|
|
|
const machine = createMachine(
|
|
{
|
|
idle: state(transition("next", "child")),
|
|
child: invoke(child, transition("done", "end")),
|
|
end: state()
|
|
},
|
|
() => ({ stuff: [] })
|
|
);
|
|
|
|
let service = interpret(machine, () => {});
|
|
service.send('next');
|
|
|
|
await wait(50)();
|
|
|
|
assert.deepEqual(service.context.stuff, [1, 2]);
|
|
});
|
|
|
|
QUnit.test('Service does not have a child when not in an invoked state', assert => {
|
|
const child = createMachine({
|
|
nestedOne: state(
|
|
transition('next', 'nestedTwo')
|
|
),
|
|
nestedTwo: state()
|
|
});
|
|
const parent = createMachine({
|
|
one: invoke(child,
|
|
transition('done', 'two')
|
|
),
|
|
two: state()
|
|
});
|
|
|
|
let service = interpret(parent, () => {});
|
|
assert.ok(service.child, 'there is a child service');
|
|
|
|
service.child.send('next');
|
|
assert.notOk(service.child, 'No longer a child');
|
|
});
|
|
|
|
QUnit.test('Multi level nested machines resolve in correct order', async assert => {
|
|
assert.expect(18);
|
|
|
|
const four = createMachine({
|
|
init: state(
|
|
transition('START', 'start'),
|
|
),
|
|
start: state(
|
|
transition('DONE', 'done'),
|
|
),
|
|
done: state(),
|
|
})
|
|
|
|
const three = createMachine({
|
|
init: state(
|
|
transition('START', 'start'),
|
|
),
|
|
start: invoke(four,
|
|
transition('done', 'done'),
|
|
),
|
|
done: state(),
|
|
})
|
|
|
|
const two = createMachine({
|
|
init: state(
|
|
transition('START', 'start'),
|
|
),
|
|
start: invoke(three,
|
|
transition('done', 'done'),
|
|
),
|
|
done: state(),
|
|
})
|
|
|
|
const one = createMachine({
|
|
init: state(
|
|
transition('START', 'start'),
|
|
),
|
|
start: invoke(two,
|
|
transition('done', 'done'),
|
|
),
|
|
done: state(),
|
|
})
|
|
|
|
let c = 0;
|
|
let service = interpret(one, thisService => {
|
|
switch (c) {
|
|
case 0:
|
|
assert.equal(service.machine.current, 'start', 'initial state');
|
|
break;
|
|
case 1:
|
|
assert.notEqual(thisService.machine.states, service.machine.states, 'second time a different service');
|
|
assert.ok(service.child, 'has child');
|
|
assert.equal(service.child.machine.current, 'start');
|
|
break;
|
|
case 2:
|
|
assert.ok(service.child.child, 'has grand child');
|
|
assert.equal(service.child.machine.current, 'start');
|
|
assert.equal(service.child.child.machine.current, 'start');
|
|
break;
|
|
case 3:
|
|
assert.ok(service.child.child.child, 'has grand grand child');
|
|
assert.equal(service.child.child.machine.current, 'start');
|
|
assert.equal(service.child.child.child.machine.current, 'start');
|
|
break;
|
|
case 4:
|
|
assert.equal(service.child.child.child.machine.current, 'done');
|
|
break;
|
|
case 5:
|
|
assert.equal(service.child.child.machine.current, 'done');
|
|
assert.equal(service.child.child.child, undefined, 'child is removed when resolved');
|
|
break;
|
|
case 6:
|
|
assert.equal(service.child.machine.current, 'done');
|
|
assert.equal(service.child.child, undefined, 'child is removed when resolved');
|
|
break;
|
|
case 7:
|
|
assert.equal(service.machine.current, 'done');
|
|
assert.equal(service.child, undefined, 'child is removed when resolved');
|
|
break;
|
|
}
|
|
c++;
|
|
});
|
|
service.send('START') // machine one
|
|
service.child.send('START') // machine two
|
|
service.child.child.send('START') // machine tree
|
|
service.child.child.child.send('START') // machine four
|
|
service.child.child.child.send('DONE') // machine four
|
|
assert.equal(c, 8, 'there were 6 transitions');
|
|
});
|
|
|
|
QUnit.test('Invoking a machine that immediately finishes', async assert => {
|
|
assert.expect(3);
|
|
const expectations = [ 'nestedTwo', 'three', 'three' ];
|
|
|
|
const child = createMachine({
|
|
nestedOne: state(
|
|
immediate('nestedTwo')
|
|
),
|
|
nestedTwo: final()
|
|
});
|
|
|
|
const parent = createMachine({
|
|
one: state(
|
|
transition('next', 'two')
|
|
),
|
|
two: invoke(child,
|
|
transition('done', 'three')
|
|
),
|
|
three: final()
|
|
});
|
|
|
|
let service = interpret(parent, s => {
|
|
assert.equal(s.machine.current, expectations.shift());
|
|
});
|
|
|
|
service.send('next');
|
|
});
|
|
});
|