phone/packages/robot3/test/test-invoke.js
Corey Johnson 27aa62f950 Upgrade robot3 to 1.3.0 for improved TypeScript support
- Bundle robot3 1.3.0 locally (not yet published to npm)
- Fix remaining type inference issues with `as any` casts for invoke() events

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 14:53:11 -08:00

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');
});
});