@moxxy/plugin-computer-control 0.41.2 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/win32-x64/moxxy-computer.exe +0 -0
- package/bin/win32-x64/moxxy-computer.exe.json +1 -1
- package/dist/backend/access.d.ts +129 -0
- package/dist/backend/access.d.ts.map +1 -0
- package/dist/backend/access.js +158 -0
- package/dist/backend/access.js.map +1 -0
- package/dist/backend/app-hints.d.ts +14 -0
- package/dist/backend/app-hints.d.ts.map +1 -0
- package/dist/backend/app-hints.js +30 -0
- package/dist/backend/app-hints.js.map +1 -0
- package/dist/backend/backend.d.ts +70 -0
- package/dist/backend/backend.d.ts.map +1 -0
- package/dist/backend/backend.js +466 -0
- package/dist/backend/backend.js.map +1 -0
- package/dist/backend/rpc.d.ts +1438 -0
- package/dist/backend/rpc.d.ts.map +1 -0
- package/dist/backend/rpc.js +75 -0
- package/dist/backend/rpc.js.map +1 -0
- package/dist/backend/turn-controls.d.ts +20 -0
- package/dist/backend/turn-controls.d.ts.map +1 -0
- package/dist/backend/turn-controls.js +115 -0
- package/dist/backend/turn-controls.js.map +1 -0
- package/dist/contract/guidance.d.ts +12 -0
- package/dist/contract/guidance.d.ts.map +1 -0
- package/dist/contract/guidance.js +42 -0
- package/dist/contract/guidance.js.map +1 -0
- package/dist/contract/image.d.ts +29 -0
- package/dist/contract/image.d.ts.map +1 -0
- package/dist/contract/image.js +46 -0
- package/dist/contract/image.js.map +1 -0
- package/dist/contract/keys.d.ts +15 -0
- package/dist/contract/keys.d.ts.map +1 -0
- package/dist/contract/keys.js +118 -0
- package/dist/contract/keys.js.map +1 -0
- package/dist/contract/outcome.d.ts +61 -0
- package/dist/contract/outcome.d.ts.map +1 -0
- package/dist/contract/outcome.js +63 -0
- package/dist/contract/outcome.js.map +1 -0
- package/dist/contract/progress.d.ts +27 -0
- package/dist/contract/progress.d.ts.map +1 -0
- package/dist/contract/progress.js +36 -0
- package/dist/contract/progress.js.map +1 -0
- package/dist/contract/tools.d.ts +338 -0
- package/dist/contract/tools.d.ts.map +1 -0
- package/dist/contract/tools.js +195 -0
- package/dist/contract/tools.js.map +1 -0
- package/dist/contract/untrusted.d.ts +3 -0
- package/dist/contract/untrusted.d.ts.map +1 -0
- package/dist/contract/untrusted.js +8 -0
- package/dist/contract/untrusted.js.map +1 -0
- package/dist/helper/artifact.d.ts +41 -0
- package/dist/helper/artifact.d.ts.map +1 -0
- package/dist/helper/artifact.js +189 -0
- package/dist/helper/artifact.js.map +1 -0
- package/dist/helper/protocol.d.ts +80 -0
- package/dist/helper/protocol.d.ts.map +1 -0
- package/dist/helper/protocol.js +49 -0
- package/dist/helper/protocol.js.map +1 -0
- package/dist/helper/transport.d.ts +43 -0
- package/dist/helper/transport.d.ts.map +1 -0
- package/dist/{windows → helper}/transport.js +45 -19
- package/dist/helper/transport.js.map +1 -0
- package/dist/index.d.ts +9 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -42
- package/dist/index.js.map +1 -1
- package/dist/jev/ladder.d.ts +32 -0
- package/dist/jev/ladder.d.ts.map +1 -0
- package/dist/jev/ladder.js +66 -0
- package/dist/jev/ladder.js.map +1 -0
- package/dist/jev/memory.d.ts +11 -0
- package/dist/jev/memory.d.ts.map +1 -0
- package/dist/jev/memory.js +16 -0
- package/dist/jev/memory.js.map +1 -0
- package/dist/jev/run.d.ts +93 -0
- package/dist/jev/run.d.ts.map +1 -0
- package/dist/jev/run.js +403 -0
- package/dist/jev/run.js.map +1 -0
- package/dist/jev/train.d.ts +97 -0
- package/dist/jev/train.d.ts.map +1 -0
- package/dist/jev/train.js +28 -0
- package/dist/jev/train.js.map +1 -0
- package/dist/linux/profile.d.ts +6 -0
- package/dist/linux/profile.d.ts.map +1 -0
- package/dist/linux/profile.js +21 -0
- package/dist/linux/profile.js.map +1 -0
- package/dist/macos/profile.d.ts +5 -0
- package/dist/macos/profile.d.ts.map +1 -0
- package/dist/macos/profile.js +17 -0
- package/dist/macos/profile.js.map +1 -0
- package/dist/preview/controller.d.ts +127 -0
- package/dist/preview/controller.d.ts.map +1 -0
- package/dist/preview/controller.js +203 -0
- package/dist/preview/controller.js.map +1 -0
- package/dist/preview/surface.d.ts +9 -0
- package/dist/preview/surface.d.ts.map +1 -0
- package/dist/preview/surface.js +50 -0
- package/dist/preview/surface.js.map +1 -0
- package/dist/windows/maintenance.d.ts +1 -1
- package/dist/windows/maintenance.d.ts.map +1 -1
- package/dist/windows/maintenance.js +6 -5
- package/dist/windows/maintenance.js.map +1 -1
- package/dist/windows/profile.d.ts +5 -0
- package/dist/windows/profile.d.ts.map +1 -0
- package/dist/windows/profile.js +17 -0
- package/dist/windows/profile.js.map +1 -0
- package/learned/README.md +26 -0
- package/learned/cases/com.apple.calculator.json +395 -0
- package/learned/cases/com.apple.finder.json +115 -0
- package/learned/cases/com.apple.safari.json +112 -0
- package/learned/cases/com.apple.systempreferences.json +255 -0
- package/learned/com.apple.calculator-eace95fc.json +334 -0
- package/learned/com.apple.finder-27cf6ce8.json +260 -0
- package/learned/com.apple.safari-7cd9df4f.json +143 -0
- package/learned/com.apple.systempreferences-02cf0b8b.json +535 -0
- package/package.json +13 -7
- package/scripts/promote-learned.mjs +8 -0
- package/scripts/summarize-trial.mjs +68 -0
- package/scripts/train-learned.mjs +64 -0
- package/skills/computer-apps/blender.md +29 -0
- package/skills/computer-apps/browsers.md +50 -0
- package/skills/computer-apps/design-tools.md +37 -0
- package/skills/computer-apps/finder.md +23 -0
- package/skills/computer-apps/office.md +46 -0
- package/skills/computer-apps/video-editors.md +44 -0
- package/skills/computer-control.md +154 -191
- package/src/backend/access.test.ts +158 -0
- package/src/backend/access.ts +168 -0
- package/src/backend/app-hints.test.ts +59 -0
- package/src/backend/app-hints.ts +39 -0
- package/src/backend/backend.test.ts +725 -0
- package/src/backend/backend.ts +482 -0
- package/src/backend/contract-helper.fixture.mjs +138 -0
- package/src/backend/helper.fixture.ts +38 -0
- package/src/backend/rpc.ts +90 -0
- package/src/backend/turn-controls.test.ts +178 -0
- package/src/backend/turn-controls.ts +120 -0
- package/src/contract/guidance.test.ts +75 -0
- package/src/contract/guidance.ts +48 -0
- package/src/contract/image.test.ts +74 -0
- package/src/contract/image.ts +54 -0
- package/src/contract/keys.test.ts +103 -0
- package/src/contract/keys.ts +117 -0
- package/src/contract/outcome.test.ts +71 -0
- package/src/contract/outcome.ts +72 -0
- package/src/contract/progress.test.ts +56 -0
- package/src/contract/progress.ts +43 -0
- package/src/contract/tools.test.ts +210 -0
- package/src/contract/tools.ts +212 -0
- package/src/contract/untrusted.test.ts +20 -0
- package/src/contract/untrusted.ts +8 -0
- package/src/helper/artifact.test.ts +131 -0
- package/src/helper/artifact.ts +182 -0
- package/src/helper/protocol.test.ts +29 -0
- package/src/helper/protocol.ts +50 -0
- package/src/{windows → helper}/transport.test.ts +76 -19
- package/src/{windows → helper}/transport.ts +54 -16
- package/src/index.test.ts +112 -0
- package/src/index.ts +36 -53
- package/src/jev/ladder.test.ts +113 -0
- package/src/jev/ladder.ts +88 -0
- package/src/jev/memory.ts +20 -0
- package/src/jev/run.test.ts +700 -0
- package/src/jev/run.ts +443 -0
- package/src/jev/train.test.ts +40 -0
- package/src/jev/train.ts +41 -0
- package/src/linux/helper.test.ts +481 -0
- package/src/linux/profile.ts +25 -0
- package/src/macos/helper.test.ts +988 -0
- package/src/macos/profile.ts +19 -0
- package/src/preview/controller.test.ts +308 -0
- package/src/preview/controller.ts +262 -0
- package/src/preview/surface.test.ts +68 -0
- package/src/preview/surface.ts +45 -0
- package/src/skill.test.ts +34 -0
- package/src/windows/maintenance.ts +8 -7
- package/src/windows/profile.ts +19 -0
- package/dist/shell.d.ts +0 -56
- package/dist/shell.d.ts.map +0 -1
- package/dist/shell.js +0 -189
- package/dist/shell.js.map +0 -1
- package/dist/temporary-files.d.ts +0 -2
- package/dist/temporary-files.d.ts.map +0 -1
- package/dist/temporary-files.js +0 -10
- package/dist/temporary-files.js.map +0 -1
- package/dist/tools/applescript.d.ts +0 -2
- package/dist/tools/applescript.d.ts.map +0 -1
- package/dist/tools/applescript.js +0 -50
- package/dist/tools/applescript.js.map +0 -1
- package/dist/tools/click.d.ts +0 -2
- package/dist/tools/click.d.ts.map +0 -1
- package/dist/tools/click.js +0 -58
- package/dist/tools/click.js.map +0 -1
- package/dist/tools/clipboard.d.ts +0 -2
- package/dist/tools/clipboard.d.ts.map +0 -1
- package/dist/tools/clipboard.js +0 -71
- package/dist/tools/clipboard.js.map +0 -1
- package/dist/tools/key.d.ts +0 -11
- package/dist/tools/key.d.ts.map +0 -1
- package/dist/tools/key.js +0 -143
- package/dist/tools/key.js.map +0 -1
- package/dist/tools/open.d.ts +0 -2
- package/dist/tools/open.d.ts.map +0 -1
- package/dist/tools/open.js +0 -91
- package/dist/tools/open.js.map +0 -1
- package/dist/tools/screenshot.d.ts +0 -2
- package/dist/tools/screenshot.d.ts.map +0 -1
- package/dist/tools/screenshot.js +0 -162
- package/dist/tools/screenshot.js.map +0 -1
- package/dist/tools/type.d.ts +0 -8
- package/dist/tools/type.d.ts.map +0 -1
- package/dist/tools/type.js +0 -65
- package/dist/tools/type.js.map +0 -1
- package/dist/windows/artifact.d.ts +0 -3
- package/dist/windows/artifact.d.ts.map +0 -1
- package/dist/windows/artifact.js +0 -57
- package/dist/windows/artifact.js.map +0 -1
- package/dist/windows/backend.d.ts +0 -11
- package/dist/windows/backend.d.ts.map +0 -1
- package/dist/windows/backend.js +0 -122
- package/dist/windows/backend.js.map +0 -1
- package/dist/windows/contracts.d.ts +0 -1627
- package/dist/windows/contracts.d.ts.map +0 -1
- package/dist/windows/contracts.js +0 -137
- package/dist/windows/contracts.js.map +0 -1
- package/dist/windows/control-service.d.ts +0 -12
- package/dist/windows/control-service.d.ts.map +0 -1
- package/dist/windows/control-service.js +0 -65
- package/dist/windows/control-service.js.map +0 -1
- package/dist/windows/guidance.d.ts +0 -3
- package/dist/windows/guidance.d.ts.map +0 -1
- package/dist/windows/guidance.js +0 -20
- package/dist/windows/guidance.js.map +0 -1
- package/dist/windows/protocol.d.ts +0 -9
- package/dist/windows/protocol.d.ts.map +0 -1
- package/dist/windows/protocol.js +0 -32
- package/dist/windows/protocol.js.map +0 -1
- package/dist/windows/transport.d.ts +0 -23
- package/dist/windows/transport.d.ts.map +0 -1
- package/dist/windows/transport.js.map +0 -1
- package/src/shell.test.ts +0 -186
- package/src/shell.ts +0 -213
- package/src/temporary-files.test.ts +0 -18
- package/src/temporary-files.ts +0 -9
- package/src/tools/applescript-serialize.test.ts +0 -74
- package/src/tools/applescript.ts +0 -53
- package/src/tools/click.ts +0 -60
- package/src/tools/clipboard.ts +0 -72
- package/src/tools/key.ts +0 -155
- package/src/tools/open.ts +0 -96
- package/src/tools/screenshot.test.ts +0 -137
- package/src/tools/screenshot.ts +0 -180
- package/src/tools/type.ts +0 -68
- package/src/tools.test.ts +0 -94
- package/src/windows/action-contracts.test.ts +0 -13
- package/src/windows/artifact.test.ts +0 -16
- package/src/windows/artifact.ts +0 -57
- package/src/windows/backend.test.ts +0 -41
- package/src/windows/backend.ts +0 -122
- package/src/windows/contracts.test.ts +0 -81
- package/src/windows/contracts.ts +0 -143
- package/src/windows/control-service.test.ts +0 -58
- package/src/windows/control-service.ts +0 -68
- package/src/windows/guidance.test.ts +0 -29
- package/src/windows/guidance.ts +0 -21
- package/src/windows/model-contract.test.ts +0 -37
- package/src/windows/protocol.ts +0 -27
- package/src/windows/text-contracts.test.ts +0 -14
- package/src/windows/window-typing.test.ts +0 -14
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { AppState } from '../backend/rpc.js';
|
|
3
|
+
import { ComputerUseError, type ActionResult } from '../contract/outcome.js';
|
|
4
|
+
import type { ComputerAction, RunStep } from '../contract/tools.js';
|
|
5
|
+
import type { AppElement } from '@moxxy/jev';
|
|
6
|
+
import { JevError, type AskJev, type JevAnswers, type JevQuestion } from '@moxxy/jev';
|
|
7
|
+
import { STATE_CHARS } from '@moxxy/jev';
|
|
8
|
+
import { describeRun, runSteps, type RunDeps } from './run.js';
|
|
9
|
+
|
|
10
|
+
const button = (index: number, title: string, extra: Partial<AppElement> = {}): AppElement =>
|
|
11
|
+
({ key: `w/${index}`, index, depth: 1, role: 'button', title, frame: { x: index * 100, y: 10, width: 80, height: 20 }, ...extra });
|
|
12
|
+
|
|
13
|
+
/** A window in memory: actions go to `react`, which may change the elements. */
|
|
14
|
+
function app(elements: AppElement[], react: (action: ComputerAction, elements: AppElement[]) => ActionResult | void = () => undefined) {
|
|
15
|
+
const acted: ComputerAction[] = [];
|
|
16
|
+
let looks = 0;
|
|
17
|
+
const state = (): AppState => ({ tree: { app: 'Editor', window: 'Main', elements: [{ key: 'w', index: 0, depth: 0, role: 'window' }, ...elements.map((element) => ({ ...element }))] } });
|
|
18
|
+
return {
|
|
19
|
+
acted, state,
|
|
20
|
+
get looks() { return looks; },
|
|
21
|
+
observe: async () => { looks += 1; return state(); },
|
|
22
|
+
act: async (action: ComputerAction) => {
|
|
23
|
+
acted.push(action);
|
|
24
|
+
return { result: react(action, elements) ?? { outcome: 'delivered' as const }, state: state() };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Judgement = (state: { step?: RunStep; performed?: RunStep; elements: string }, id: string) => JevAnswers[string] | undefined;
|
|
30
|
+
const pick = (index: number | 'none', probability = 0.9): JevAnswers[string] =>
|
|
31
|
+
({ type: 'choice', choice: String(index), probabilities: { [String(index)]: probability }, confidence: probability });
|
|
32
|
+
const yes = (noul: number): JevAnswers[string] => ({ type: 'noul', noul });
|
|
33
|
+
|
|
34
|
+
/** Jev scripted per question; records what each request asked. */
|
|
35
|
+
function jev(judgement: Judgement) {
|
|
36
|
+
const requests: Array<{ state: Record<string, unknown>; ids: string[] }> = [];
|
|
37
|
+
const ask: AskJev = async (state, questions: Readonly<Record<string, JevQuestion>>) => {
|
|
38
|
+
requests.push({ state: state as Record<string, unknown>, ids: Object.keys(questions) });
|
|
39
|
+
return Object.fromEntries(Object.entries(questions).map(([id, question]) =>
|
|
40
|
+
[id, judgement(state as never, id) ?? (question.type === 'choice' ? pick('none') : yes(0))]));
|
|
41
|
+
};
|
|
42
|
+
return { ask, requests };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const deps = (window: ReturnType<typeof app>, ask: AskJev): RunDeps =>
|
|
46
|
+
({ ask, observe: window.observe, act: window.act, selectAll: 'super+a', signal: new AbortController().signal });
|
|
47
|
+
|
|
48
|
+
describe('runSteps', () => {
|
|
49
|
+
it('finds each element on the live window and acts on it, one decision per step', async () => {
|
|
50
|
+
const window = app([button(1, 'Export'), button(2, 'Cancel')], (action, elements) => {
|
|
51
|
+
if (action.action === 'click' && action.element_index === 1) elements.push(button(3, 'Save'));
|
|
52
|
+
if (action.action === 'click' && action.element_index === 3) elements.push(button(4, 'Saved'));
|
|
53
|
+
});
|
|
54
|
+
const { ask, requests } = jev((state, id) => {
|
|
55
|
+
if (id !== 'target') return undefined;
|
|
56
|
+
return state.step?.target === 'the button that exports the clip' ? pick(1) : state.elements.includes('"Save"') ? pick(3) : pick('none');
|
|
57
|
+
});
|
|
58
|
+
const report = await runSteps('Export the clip', [{ do: 'click', target: 'the button that exports the clip' }, { do: 'click', target: 'the button that saves it' }], window.state(), deps(window, ask));
|
|
59
|
+
expect(window.acted).toEqual([
|
|
60
|
+
{ action: 'click', element_index: 1, mouse_button: 'left', click_count: 1 },
|
|
61
|
+
{ action: 'click', element_index: 3, mouse_button: 'left', click_count: 1 },
|
|
62
|
+
]);
|
|
63
|
+
expect(report.outcomes.map((outcome) => outcome.status)).toEqual(['done', 'done']);
|
|
64
|
+
expect(report.outcomes[0]?.element).toBe('[1] button "Export"');
|
|
65
|
+
// The second step's element is asked for in the request that follows the first action.
|
|
66
|
+
expect(requests.map((request) => request.ids)).toEqual([['target'], ['target']]);
|
|
67
|
+
expect(requests[0]?.state).toMatchObject({ goal: 'Export the clip', app: 'Editor', step: { do: 'click', target: 'the button that exports the clip' } });
|
|
68
|
+
expect(report.asks).toBe(2);
|
|
69
|
+
expect(report.state.tree.elements).toHaveLength(5);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('says where its time went: Jev, the actions and the looks at the window', async () => {
|
|
73
|
+
const pause = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
74
|
+
const window = app([button(1, 'Export')]);
|
|
75
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : undefined));
|
|
76
|
+
const report = await runSteps('Export', [{ do: 'click', target: 'the Export button' }], window.state(), {
|
|
77
|
+
...deps(window, ask),
|
|
78
|
+
ask: async (...args) => { await pause(30); return ask(...args); },
|
|
79
|
+
act: async (action) => { await pause(20); return window.act(action); },
|
|
80
|
+
});
|
|
81
|
+
expect(report.time.jev).toBeGreaterThanOrEqual(25);
|
|
82
|
+
expect(report.time.act).toBeGreaterThanOrEqual(15);
|
|
83
|
+
expect(report.time.look).toBeGreaterThanOrEqual(0);
|
|
84
|
+
expect(report.time.jev + report.time.act + report.time.look).toBeLessThanOrEqual(report.ms + 1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Jev reads elements; a picture is what shows a click that changed only pixels, and what the model reads at the end.
|
|
88
|
+
it('asks for a picture of the window only around clicks and at the end', async () => {
|
|
89
|
+
const window = app([button(1, 'Note', { role: 'text area', value: '' }), button(2, 'Send')], (action, elements) => {
|
|
90
|
+
if (action.action === 'type_text') (elements[0] as AppElement).value += action.text;
|
|
91
|
+
if (action.action === 'click') elements.push(button(3, 'Sent'));
|
|
92
|
+
});
|
|
93
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(2) : undefined));
|
|
94
|
+
const pictures: Array<[string, boolean | undefined]> = [];
|
|
95
|
+
const looks: Array<boolean | undefined> = [];
|
|
96
|
+
const steps: RunStep[] = [{ do: 'type', text: 'hi' }, { do: 'key', key: 'Tab' }, { do: 'click', target: 'Send' }, { do: 'key', key: 'Return' }];
|
|
97
|
+
await runSteps('Send', steps, window.state(), {
|
|
98
|
+
...deps(window, ask),
|
|
99
|
+
observe: async (picture) => { looks.push(picture); return window.observe(); },
|
|
100
|
+
act: async (action, { picture }) => { pictures.push([action.action, picture]); return window.act(action); },
|
|
101
|
+
});
|
|
102
|
+
expect(pictures).toEqual([['type_text', false], ['press_key', true], ['click', true], ['press_key', true]]);
|
|
103
|
+
expect(looks).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('judges a key that expects something by its picture too: selecting text changes pixels and no element', async () => {
|
|
107
|
+
let selected = false;
|
|
108
|
+
const field = button(1, 'Address', { role: 'text field', value: 'olx.pl', states: ['focused'] });
|
|
109
|
+
const look = (picture: boolean): AppState => ({
|
|
110
|
+
tree: { app: 'Safari', window: 'OLX', elements: [{ key: 'w', index: 0, depth: 0, role: 'window' }, field] },
|
|
111
|
+
...(picture ? { screenshot: { mediaType: 'image/jpeg', base64: selected ? 'c2VsZWN0ZWQ=' : 'cGxhaW4=', width: 10, height: 10 } } : {}),
|
|
112
|
+
});
|
|
113
|
+
const { ask } = jev((_state, id) => (id === 'expected' ? yes(0.4) : undefined));
|
|
114
|
+
const steps: RunStep[] = [{ do: 'key', key: 'super+l' }, { do: 'key', key: 'super+a', expect: 'the address is selected' }, { do: 'key', key: 'Tab' }, { do: 'key', key: 'Tab' }];
|
|
115
|
+
const report = await runSteps('Select the address', steps, look(true), {
|
|
116
|
+
ask, signal: new AbortController().signal, observe: async (picture = true) => look(picture),
|
|
117
|
+
act: async (action, { picture }) => {
|
|
118
|
+
if (action.action === 'press_key' && action.key === 'super+a') selected = true;
|
|
119
|
+
return { result: { outcome: 'delivered' }, state: look(picture) };
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
expect(report.outcomes.map((outcome) => outcome.status)).toEqual(['done', 'done', 'done', 'done']);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('keeps what it shows Jev within its input limit when a long page changes as a whole', async () => {
|
|
126
|
+
// The first click changes every listing of the page; the second opens another page.
|
|
127
|
+
let clicks = 0;
|
|
128
|
+
const listing = (index: number) => button(index, `listing ${index} seen ${clicks} ${'x'.repeat(60)}`, { role: 'link' });
|
|
129
|
+
const look = (): AppState => ({ tree: { app: 'Safari', window: clicks < 2 ? 'Listings' : 'Listing', elements: [{ key: 'w', index: 0, depth: 0, role: 'window' }, ...Array.from({ length: 900 }, (_, at) => listing(at + 1))] } });
|
|
130
|
+
const sizes: number[] = [];
|
|
131
|
+
const ask: AskJev = async (state, questions) => {
|
|
132
|
+
const { elements, changes } = state as { elements: string; changes?: string };
|
|
133
|
+
sizes.push(elements.length + (changes?.length ?? 0));
|
|
134
|
+
return Object.fromEntries(Object.keys(questions).map((id) => [id, id.startsWith('target') ? pick(1) : yes(id === 'expected' ? 0.9 : 0)]));
|
|
135
|
+
};
|
|
136
|
+
const steps: RunStep[] = [{ do: 'click', target: 'the first listing', expect: 'the listing opens' }, { do: 'click', target: 'the first listing', expect: 'the listing opens' }];
|
|
137
|
+
await runSteps('Open', steps, look(), {
|
|
138
|
+
ask, signal: new AbortController().signal, observe: async () => look(),
|
|
139
|
+
act: async () => { clicks += 1; return { result: { outcome: 'delivered' }, state: look() }; },
|
|
140
|
+
});
|
|
141
|
+
expect(clicks).toBe(2);
|
|
142
|
+
for (const size of sizes) expect(size).toBeLessThanOrEqual(STATE_CHARS);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('steps that check nothing and need no element', () => {
|
|
146
|
+
const typing = () => app([button(1, 'Address', { role: 'text field', value: '', states: ['focused'] })], (action, elements) => {
|
|
147
|
+
if (action.action === 'type_text') (elements[0] as AppElement).value += action.text;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('go to the helper together, with one state at the end', async () => {
|
|
151
|
+
const window = typing();
|
|
152
|
+
const batches: ComputerAction[][] = [];
|
|
153
|
+
const { ask } = jev((_state, id) => (id === 'expected' ? yes(0.9) : undefined));
|
|
154
|
+
const steps: RunStep[] = [{ do: 'key', key: 'super+l' }, { do: 'type', text: 'olx.pl' }, { do: 'key', key: 'Return', expect: 'OLX opens' }];
|
|
155
|
+
const report = await runSteps('Open OLX', steps, window.state(), {
|
|
156
|
+
...deps(window, ask),
|
|
157
|
+
batch: async (actions) => { batches.push([...actions]); for (const action of actions) await window.act(action); return { results: actions.map(() => ({ outcome: 'delivered' as const })), state: window.state() }; },
|
|
158
|
+
});
|
|
159
|
+
expect(batches).toEqual([[{ action: 'press_key', key: 'super+l', repeat: 1 }, { action: 'type_text', text: 'olx.pl' }]]);
|
|
160
|
+
expect(window.acted.at(-1)).toEqual({ action: 'press_key', key: 'Return', repeat: 1 });
|
|
161
|
+
expect(report.outcomes.map((outcome) => outcome.status)).toEqual(['done', 'done', 'verified']);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('stop at the first one the helper did not deliver', async () => {
|
|
165
|
+
const window = typing();
|
|
166
|
+
const steps: RunStep[] = [{ do: 'key', key: 'super+l' }, { do: 'type', text: 'a' }, { do: 'type', text: 'b' }];
|
|
167
|
+
const report = await runSteps('Type', steps, window.state(), {
|
|
168
|
+
...deps(window, jev(() => undefined).ask),
|
|
169
|
+
batch: async () => ({ results: [{ outcome: 'delivered' }, { outcome: 'blocked', code: 'target_blocked' }], state: window.state() }),
|
|
170
|
+
});
|
|
171
|
+
expect(report.outcomes).toMatchObject([{ status: 'done' }, { status: 'failed', why: 'blocked (target_blocked)' }]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('stop at a step the helper did not report on instead of sending it again', async () => {
|
|
175
|
+
const window = typing();
|
|
176
|
+
let batches = 0;
|
|
177
|
+
const steps: RunStep[] = [{ do: 'key', key: 'super+l' }, { do: 'type', text: 'a' }];
|
|
178
|
+
const report = await runSteps('Type', steps, window.state(), {
|
|
179
|
+
...deps(window, jev(() => undefined).ask),
|
|
180
|
+
batch: async () => { batches += 1; return { results: [{ outcome: 'delivered' }], state: window.state() }; },
|
|
181
|
+
});
|
|
182
|
+
expect(batches).toBe(1);
|
|
183
|
+
expect(window.acted).toEqual([]);
|
|
184
|
+
expect(report.outcomes).toMatchObject([{ status: 'done' }, { status: 'failed', why: 'the helper did not say whether this step was done' }]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('acts on the one element named exactly as the target without asking Jev where it is', async () => {
|
|
189
|
+
const window = app([button(1, 'Export'), button(2, 'Cancel')], (action, elements) => {
|
|
190
|
+
if (action.action === 'click' && action.element_index === 1) elements.push(button(3, 'Saved'));
|
|
191
|
+
});
|
|
192
|
+
const { ask, requests } = jev((_state, id) => (id === 'expected' ? yes(0.9) : undefined));
|
|
193
|
+
const report = await runSteps('Export', [{ do: 'click', target: 'Export button', expect: 'saved' }], window.state(), deps(window, ask));
|
|
194
|
+
expect(window.acted).toEqual([{ action: 'click', element_index: 1, mouse_button: 'left', click_count: 1 }]);
|
|
195
|
+
expect(requests.flatMap((request) => request.ids)).not.toContain('target');
|
|
196
|
+
expect(report.outcomes).toMatchObject([{ status: 'verified', element: '[1] button "Export"' }]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('asks Jev where the element is when the one named as the target does not do the step', async () => {
|
|
200
|
+
const window = app([button(1, 'Export'), button(2, 'Export as file')], (action, elements) => {
|
|
201
|
+
if (action.action === 'click' && action.element_index === 2) elements.push(button(3, 'Saved'));
|
|
202
|
+
});
|
|
203
|
+
const { ask, requests } = jev((state, id) => (id === 'target' ? pick(2) : id === 'expected' ? yes(state.elements.includes('"Saved"') ? 0.9 : 0) : undefined));
|
|
204
|
+
const report = await runSteps('Export', [{ do: 'click', target: 'Export', expect: 'saved' }], window.state(), deps(window, ask));
|
|
205
|
+
expect(window.acted.map((action) => 'element_index' in action && action.element_index)).toEqual([1, 2]);
|
|
206
|
+
expect(requests.map((request) => request.ids)).toEqual([['already'], ['expected'], ['target', 'already'], ['expected']]);
|
|
207
|
+
expect(report.outcomes).toMatchObject([{ status: 'verified', element: '[2] button "Export as file"' }]);
|
|
208
|
+
expect(report.outcomes[0]).not.toHaveProperty('stale');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('asks Jev nothing for keys and typing into the focus', async () => {
|
|
212
|
+
const window = app([button(1, 'Note', { role: 'text area', value: '' })], (action, elements) => {
|
|
213
|
+
if (action.action === 'type_text') (elements[0] as AppElement).value = action.text;
|
|
214
|
+
});
|
|
215
|
+
const { ask, requests } = jev(() => undefined);
|
|
216
|
+
const report = await runSteps('Type', [{ do: 'type', text: 'hello' }, { do: 'key', key: 'Return' }], window.state(), deps(window, ask));
|
|
217
|
+
expect(window.acted).toEqual([{ action: 'type_text', text: 'hello' }, { action: 'press_key', key: 'Return', repeat: 1 }]);
|
|
218
|
+
expect(requests).toEqual([]);
|
|
219
|
+
expect(report.outcomes.map((outcome) => outcome.status)).toEqual(['done', 'done']);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('does not click again when a click at a point changed only the screenshot: a menu the elements do not list would close', async () => {
|
|
223
|
+
const acted: ComputerAction[] = [];
|
|
224
|
+
let open = false;
|
|
225
|
+
const state = (): AppState => ({
|
|
226
|
+
tree: { app: 'Safari', window: 'Listings', elements: [{ key: 'w', index: 0, depth: 0, role: 'window' }, button(1, 'Sort'), button(2, 'Sort label')] },
|
|
227
|
+
screenshot: { mediaType: 'image/jpeg', base64: open ? 'b3Blbg==' : 'Y2xvc2Vk', width: 10, height: 10 },
|
|
228
|
+
});
|
|
229
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : undefined));
|
|
230
|
+
const report = await runSteps('Sort the listings', [{ do: 'click', target: 'the sort menu', expect: 'the sort options show' }], state(), {
|
|
231
|
+
ask, selectAll: 'super+a', signal: new AbortController().signal, observe: async () => state(),
|
|
232
|
+
act: async (action) => {
|
|
233
|
+
acted.push(action);
|
|
234
|
+
// Only a real click reaches the page's menu, and every one toggles it.
|
|
235
|
+
if (action.action === 'click' && 'x' in action) open = !open;
|
|
236
|
+
return { result: { outcome: 'delivered' }, state: state() };
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
expect(acted).toEqual([
|
|
240
|
+
{ action: 'click', element_index: 1, mouse_button: 'left', click_count: 1 },
|
|
241
|
+
{ action: 'click', x: 140, y: 20, mouse_button: 'left', click_count: 1 },
|
|
242
|
+
]);
|
|
243
|
+
expect(open).toBe(true);
|
|
244
|
+
expect(report.outcomes).toMatchObject([{ status: 'failed', attempts: 2, why: expect.stringMatching(/screenshot/) }]);
|
|
245
|
+
expect(report.state.screenshot?.base64).toBe('b3Blbg==');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('skips a step whose expected result already shows', async () => {
|
|
249
|
+
const window = app([button(1, 'Export')]);
|
|
250
|
+
const { ask } = jev((_state, id) => (id === 'already' ? yes(0.95) : id === 'target' ? pick(1) : undefined));
|
|
251
|
+
const report = await runSteps('Open export', [{ do: 'click', target: 'Export', expect: 'the export dialog is open' }], window.state(), deps(window, ask));
|
|
252
|
+
expect(window.acted).toEqual([]);
|
|
253
|
+
expect(report.outcomes).toMatchObject([{ status: 'skipped', attempts: 0 }]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('checks what a step expects and marks it verified', async () => {
|
|
257
|
+
const window = app([button(1, 'Export')], (_action, elements) => { elements.push(button(2, 'Dialog')); });
|
|
258
|
+
const { ask, requests } = jev((_state, id) => (id === 'target' ? pick(1) : id === 'expected' ? yes(0.9) : undefined));
|
|
259
|
+
const report = await runSteps('Open export', [{ do: 'click', target: 'the button that exports', expect: 'a dialog is open' }], window.state(), deps(window, ask));
|
|
260
|
+
expect(report.outcomes).toMatchObject([{ status: 'verified', attempts: 1 }]);
|
|
261
|
+
expect(requests.map((request) => request.ids)).toEqual([['target', 'already'], ['expected']]);
|
|
262
|
+
expect(requests[1]?.state).toMatchObject({ performed: { do: 'click', target: 'the button that exports' } });
|
|
263
|
+
expect(String(requests[1]?.state.changes)).toContain('+ [2] button "Dialog"');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('tries the next way when the first one changes nothing', async () => {
|
|
267
|
+
const window = app([button(1, 'Export')], (action, elements) => {
|
|
268
|
+
if (action.action === 'click' && action.x !== undefined) elements.push(button(2, 'Dialog'));
|
|
269
|
+
});
|
|
270
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : undefined));
|
|
271
|
+
const report = await runSteps('Open export', [{ do: 'click', target: 'the button that exports' }], window.state(), deps(window, ask));
|
|
272
|
+
expect(window.acted).toEqual([
|
|
273
|
+
{ action: 'click', element_index: 1, mouse_button: 'left', click_count: 1 },
|
|
274
|
+
{ action: 'click', x: 140, y: 20, mouse_button: 'left', click_count: 1 },
|
|
275
|
+
]);
|
|
276
|
+
expect(report.outcomes).toMatchObject([{ status: 'done', attempts: 2 }]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('sets a value the long way when the field does not take it directly', async () => {
|
|
280
|
+
const window = app([button(1, 'Name', { role: 'text field', value: 'old' })], (action, elements) => {
|
|
281
|
+
if (action.action === 'set_value') return { outcome: 'unsupported', code: 'unsupported_action' };
|
|
282
|
+
if (action.action === 'type_text') (elements[0] as AppElement).value = action.text;
|
|
283
|
+
return undefined;
|
|
284
|
+
});
|
|
285
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : undefined));
|
|
286
|
+
const report = await runSteps('Rename', [{ do: 'set_value', target: 'the name field', text: 'new' }], window.state(), deps(window, ask));
|
|
287
|
+
expect(window.acted.map((action) => action.action)).toEqual(['set_value', 'click', 'press_key', 'type_text']);
|
|
288
|
+
expect(report.outcomes).toMatchObject([{ status: 'done', attempts: 2 }]);
|
|
289
|
+
expect(report.state.tree.elements[1]?.value).toBe('new');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('moves to the next best element when every way on the first fails, then stops and leaves the rest', async () => {
|
|
293
|
+
const window = app([button(1, 'Export'), button(2, 'Export all')]);
|
|
294
|
+
const { ask } = jev((_state, id) => (id === 'target' ? { type: 'choice', choice: '1', probabilities: { 1: 0.6, 2: 0.3, none: 0.1 }, confidence: 0.5 } : undefined));
|
|
295
|
+
const steps: RunStep[] = [{ do: 'click', target: 'Export' }, { do: 'key', key: 'Return' }];
|
|
296
|
+
const report = await runSteps('Export', steps, window.state(), deps(window, ask));
|
|
297
|
+
expect(window.acted.map((action) => ('element_index' in action ? action.element_index : 'point'))).toEqual([1, 'point', 2, 'point']);
|
|
298
|
+
expect(report.outcomes).toMatchObject([{ status: 'failed', attempts: 4, why: 'nothing changed' }]);
|
|
299
|
+
expect(describeRun(report, steps)).toMatch(/0 of 2 steps[\s\S]*1\. FAILED[\s\S]*nothing changed[\s\S]*Not run: step 2/);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('looks once more when no element matches, then names the closest ones', async () => {
|
|
303
|
+
const window = app([button(1, 'Import')]);
|
|
304
|
+
const { ask } = jev((_state, id) => (id === 'target' ? { type: 'choice', choice: 'none', probabilities: { none: 0.8, 1: 0.2 }, confidence: 0.6 } : undefined));
|
|
305
|
+
const report = await runSteps('Export', [{ do: 'click', target: 'Export' }], window.state(), deps(window, ask));
|
|
306
|
+
expect(window.looks).toBe(1);
|
|
307
|
+
expect(window.acted).toEqual([]);
|
|
308
|
+
expect(report.outcomes).toMatchObject([{ status: 'failed', attempts: 0, why: 'no element matches', closest: ['[1] button "Import"'] }]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('clicks the text the screenshot shows when no element carries that name', async () => {
|
|
312
|
+
const window = app([button(1, 'Title, Heading')], (action, elements) => {
|
|
313
|
+
if (action.action === 'click' && 'x' in action) elements.push(button(2, 'Heading added'));
|
|
314
|
+
});
|
|
315
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick('none') : undefined));
|
|
316
|
+
let reads = 0;
|
|
317
|
+
const report = await runSteps('Add a title', [{ do: 'click', target: 'przycisk „Dodaj tytuł”' }], window.state(), {
|
|
318
|
+
...deps(window, ask),
|
|
319
|
+
readText: async () => { reads += 1; return [{ text: 'Dodaj tytul', x: 100, y: 20, width: 80, height: 20 }]; },
|
|
320
|
+
});
|
|
321
|
+
expect(window.acted).toEqual([{ action: 'click', x: 140, y: 30, mouse_button: 'left', click_count: 1 }]);
|
|
322
|
+
expect(reads).toBe(1);
|
|
323
|
+
expect(report.outcomes).toMatchObject([{ status: 'done', element: 'text "Dodaj tytul" in the screenshot at 140,30' }]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('still fails when the screenshot does not read as the target either', async () => {
|
|
327
|
+
const window = app([button(1, 'Import')]);
|
|
328
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick('none') : undefined));
|
|
329
|
+
const report = await runSteps('Export', [{ do: 'click', target: 'Export' }], window.state(), {
|
|
330
|
+
...deps(window, ask), readText: async () => [{ text: 'Import', x: 0, y: 0, width: 50, height: 20 }],
|
|
331
|
+
});
|
|
332
|
+
expect(window.acted).toEqual([]);
|
|
333
|
+
expect(report.outcomes).toMatchObject([{ status: 'failed', why: 'no element matches' }]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('stops at once when the user takes over', async () => {
|
|
337
|
+
const window = app([button(1, 'Export')], () => ({ outcome: 'blocked', code: 'user_intervened' }));
|
|
338
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : undefined));
|
|
339
|
+
const report = await runSteps('Export', [{ do: 'click', target: 'Export' }], window.state(), deps(window, ask));
|
|
340
|
+
expect(window.acted).toHaveLength(1);
|
|
341
|
+
expect(report.outcomes).toMatchObject([{ status: 'failed', why: 'blocked (user_intervened)' }]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('treats a refused action as a failed way and looks again before the next one', async () => {
|
|
345
|
+
const window = app([button(1, 'Export')]);
|
|
346
|
+
let refused = false;
|
|
347
|
+
const act: RunDeps['act'] = async (action) => {
|
|
348
|
+
if (!refused) { refused = true; throw new ComputerUseError('stale_state', 'Index 1 is from an older state'); }
|
|
349
|
+
return window.act(action);
|
|
350
|
+
};
|
|
351
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : undefined));
|
|
352
|
+
const report = await runSteps('Export', [{ do: 'key', key: 'Return' }, { do: 'click', target: 'Export' }], window.state(), { ...deps(window, ask), act });
|
|
353
|
+
expect(window.looks).toBe(1);
|
|
354
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'failed', why: 'blocked (stale_state)' });
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Jev reads one window: a window or tab that is new looks like any other, so it is never sure the key worked.
|
|
358
|
+
it('goes on after a key that brought another window to the front, without pressing it again', async () => {
|
|
359
|
+
let front = 'Listings';
|
|
360
|
+
const state = (): AppState => ({ tree: { app: 'Safari', window: front, elements: [{ key: `w/${front}`, index: 0, depth: 0, role: 'window', title: front }] } });
|
|
361
|
+
const { ask } = jev((_state, id) => (id === 'expected' ? yes(0.2) : undefined));
|
|
362
|
+
const acted: ComputerAction[] = [];
|
|
363
|
+
const steps: RunStep[] = [{ do: 'key', key: 'super+n', expect: 'a new window is open' }, { do: 'key', key: 'super+l' }];
|
|
364
|
+
const report = await runSteps('Open a new window', steps, state(), {
|
|
365
|
+
ask, selectAll: 'super+a', signal: new AbortController().signal, observe: async () => state(),
|
|
366
|
+
act: async (action) => { acted.push(action); if (action.action === 'press_key') front = 'Start Page'; return { result: { outcome: 'delivered' }, state: state() }; },
|
|
367
|
+
});
|
|
368
|
+
expect(acted.map((action) => action.action)).toEqual(['press_key', 'press_key']);
|
|
369
|
+
expect(report.outcomes).toMatchObject([{ status: 'done', attempts: 1 }, { status: 'done' }]);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// An empty new window reads like the empty one in front before it ("Start Page"): only its id tells them apart.
|
|
373
|
+
it('goes on after a key that brought another window with the same title to the front', async () => {
|
|
374
|
+
let id = '101';
|
|
375
|
+
const state = (): AppState => ({ tree: { app: 'Safari', window: 'Start Page', windowId: id, elements: [{ key: 'w/start', index: 0, depth: 0, role: 'window', title: 'Start Page' }] } });
|
|
376
|
+
const { ask } = jev((_state, question) => (question === 'expected' ? yes(0.2) : undefined));
|
|
377
|
+
const acted: ComputerAction[] = [];
|
|
378
|
+
const report = await runSteps('Open a new window', [{ do: 'key', key: 'super+n', expect: 'a new window is open' }], state(), {
|
|
379
|
+
ask, selectAll: 'super+a', signal: new AbortController().signal, observe: async () => state(),
|
|
380
|
+
act: async (action) => { acted.push(action); id = '102'; return { result: { outcome: 'delivered' }, state: state() }; },
|
|
381
|
+
});
|
|
382
|
+
expect(acted).toHaveLength(1);
|
|
383
|
+
expect(report.outcomes).toMatchObject([{ status: 'done', attempts: 1 }]);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// A new tab stays in its window: the number is the same, the title is the new tab's.
|
|
387
|
+
it('goes on after a key that brought another tab of the same window to the front', async () => {
|
|
388
|
+
let front = 'Kraków – Wikipedia';
|
|
389
|
+
const state = (): AppState => ({ tree: { app: 'Safari', window: front, windowId: '101', elements: [{ key: 'w/tab', index: 0, depth: 0, role: 'window', title: front }] } });
|
|
390
|
+
const { ask } = jev((_state, question) => (question === 'expected' ? yes(0.2) : undefined));
|
|
391
|
+
const report = await runSteps('Open a new tab', [{ do: 'key', key: 'super+t', expect: 'a new tab is open' }], state(), {
|
|
392
|
+
ask, selectAll: 'super+a', signal: new AbortController().signal, observe: async () => state(),
|
|
393
|
+
act: async () => { front = 'Start Page'; return { result: { outcome: 'delivered' }, state: state() }; },
|
|
394
|
+
});
|
|
395
|
+
expect(report.outcomes).toMatchObject([{ status: 'done', attempts: 1 }]);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('stops at a key whose result does not show while the same window stays in front', async () => {
|
|
399
|
+
const window = app([button(1, 'Export')]);
|
|
400
|
+
const { ask } = jev((_state, id) => (id === 'expected' ? yes(0.1) : undefined));
|
|
401
|
+
const report = await runSteps('Export', [{ do: 'key', key: 'super+e', expect: 'an export dialog is open' }], window.state(), deps(window, ask));
|
|
402
|
+
expect(window.acted).toHaveLength(1);
|
|
403
|
+
expect(report.outcomes).toMatchObject([{ status: 'failed', attempts: 1 }]);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Typing again appends: "https://olx.plhttps://olx.pl".
|
|
407
|
+
it('does not type again once the text is in the field, whatever else the step expects', async () => {
|
|
408
|
+
const window = app([button(1, 'Address', { role: 'text field', value: '' })], (action, elements) => {
|
|
409
|
+
if (action.action === 'type_text') (elements[0] as AppElement).value += action.text;
|
|
410
|
+
});
|
|
411
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : id === 'expected' ? yes(0.1) : undefined));
|
|
412
|
+
const report = await runSteps('Open OLX', [{ do: 'type', target: 'the address field', text: 'https://olx.pl', expect: 'OLX opens' }], window.state(), deps(window, ask));
|
|
413
|
+
expect(window.acted).toEqual([{ action: 'type_text', element_index: 1, text: 'https://olx.pl' }]);
|
|
414
|
+
expect(report.state.tree.elements[1]?.value).toBe('https://olx.pl');
|
|
415
|
+
expect(report.outcomes).toMatchObject([{ status: 'done', attempts: 1 }]);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('does not type into a field that already holds exactly the text: it would append it a second time', async () => {
|
|
419
|
+
const window = app([button(1, 'Address', { role: 'text field', value: 'https://www.olx.pl', states: ['focused'] })], (action, elements) => {
|
|
420
|
+
if (action.action === 'type_text') (elements[0] as AppElement).value += action.text;
|
|
421
|
+
});
|
|
422
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : id === 'expected' ? yes(0.1) : undefined));
|
|
423
|
+
const steps: RunStep[] = [{ do: 'type', target: 'the address field', text: 'https://www.olx.pl', expect: 'OLX opens' }, { do: 'key', key: 'Return' }];
|
|
424
|
+
const report = await runSteps('Open OLX', steps, window.state(), deps(window, ask));
|
|
425
|
+
expect(window.acted).toEqual([{ action: 'press_key', key: 'Return', repeat: 1 }]);
|
|
426
|
+
expect(report.outcomes).toMatchObject([{ status: 'skipped', attempts: 0 }, { status: 'done' }]);
|
|
427
|
+
expect(describeRun(report, steps)).toContain('the field already holds this text');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('types the other way when the first left the field without the text', async () => {
|
|
431
|
+
const window = app([button(1, 'Address', { role: 'text field', value: '' })], (action, elements) => {
|
|
432
|
+
if (action.action === 'type_text' && action.element_index === undefined) (elements[0] as AppElement).value += action.text;
|
|
433
|
+
});
|
|
434
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : undefined));
|
|
435
|
+
const report = await runSteps('Open OLX', [{ do: 'type', target: 'the address field', text: 'https://olx.pl' }], window.state(), deps(window, ask));
|
|
436
|
+
expect(window.acted.map((action) => action.action)).toEqual(['type_text', 'click', 'type_text']);
|
|
437
|
+
expect(report.state.tree.elements[1]?.value).toBe('https://olx.pl');
|
|
438
|
+
expect(report.outcomes).toMatchObject([{ status: 'done', attempts: 2 }]);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('hands back to the main model when Jev cannot be asked', async () => {
|
|
442
|
+
const window = app([button(1, 'Export')]);
|
|
443
|
+
const ask: AskJev = async () => { throw new JevError(401, 'Jev returned HTTP 401'); };
|
|
444
|
+
const report = await runSteps('Export', [{ do: 'click', target: 'the button that exports' }], window.state(), deps(window, ask));
|
|
445
|
+
expect(window.acted).toEqual([]);
|
|
446
|
+
expect(report.outcomes).toMatchObject([{ status: 'failed', why: 'Jev returned HTTP 401' }]);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
describe('a result that shows late', () => {
|
|
451
|
+
it('is looked for once more before another way is tried', async () => {
|
|
452
|
+
const elements = [button(1, 'Privacy')];
|
|
453
|
+
const window = app(elements);
|
|
454
|
+
const late = async () => { if (window.acted.length > 0 && elements.length === 1) elements.push(button(2, 'Privacy pane')); return window.observe(); };
|
|
455
|
+
const { ask, requests } = jev((state, id) => (id === 'target' ? pick(1) : id === 'expected' ? yes(state.elements.includes('Privacy pane') ? 0.9 : 0.1) : undefined));
|
|
456
|
+
const report = await runSteps('Open Privacy', [{ do: 'click', target: 'the Privacy item of the sidebar', expect: 'the Privacy pane shows' }], window.state(), { ...deps(window, ask), observe: late });
|
|
457
|
+
expect(window.acted).toHaveLength(1);
|
|
458
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'verified', attempts: 1 });
|
|
459
|
+
expect(requests.map((request) => request.ids)).toEqual([['target', 'already'], ['expected'], ['expected']]);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('is not waited for twice: a step that still does not show its result goes another way', async () => {
|
|
463
|
+
const window = app([button(1, 'Privacy')]);
|
|
464
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : undefined));
|
|
465
|
+
const report = await runSteps('Open Privacy', [{ do: 'click', target: 'Privacy', expect: 'the Privacy pane shows' }], window.state(), deps(window, ask));
|
|
466
|
+
expect(window.acted).toHaveLength(2);
|
|
467
|
+
expect(window.looks).toBe(2);
|
|
468
|
+
expect(report.outcomes[0]?.status).toBe('failed');
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe('what was learned before', () => {
|
|
473
|
+
it('acts on a remembered element without asking where it is', async () => {
|
|
474
|
+
const window = app([button(1, 'Export'), button(2, 'Cancel')], (_action, elements) => { elements.push(button(3, 'Sheet')); });
|
|
475
|
+
const { ask, requests } = jev(() => undefined);
|
|
476
|
+
const known: RunDeps['known'] = (step, tree) => (step.target === 'Export' ? { element: tree.elements[1] as AppElement, way: 0 } : undefined);
|
|
477
|
+
const report = await runSteps('Export', [{ do: 'click', target: 'Export' }], window.state(), { ...deps(window, ask), known });
|
|
478
|
+
expect(window.acted).toEqual([{ action: 'click', element_index: 1, mouse_button: 'left', click_count: 1 }]);
|
|
479
|
+
expect(requests).toEqual([]);
|
|
480
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'done', recalled: true });
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('starts with the way that worked last time', async () => {
|
|
484
|
+
const window = app([button(1, 'Export')], (_action, elements) => { elements.push(button(3, 'Sheet')); });
|
|
485
|
+
const { ask } = jev(() => undefined);
|
|
486
|
+
const known: RunDeps['known'] = (_step, tree) => ({ element: tree.elements[1] as AppElement, way: 1 });
|
|
487
|
+
await runSteps('Export', [{ do: 'click', target: 'Export' }], window.state(), { ...deps(window, ask), known });
|
|
488
|
+
expect(window.acted).toEqual([{ action: 'click', x: 140, y: 20, mouse_button: 'left', click_count: 1 }]);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('reports what a verified step used, so it can be remembered', async () => {
|
|
492
|
+
const window = app([button(1, 'Export')], (action, elements) => { if (action.action === 'click') elements.push(button(2, 'Sheet')); });
|
|
493
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : id === 'expected' ? yes(0.9) : undefined));
|
|
494
|
+
const report = await runSteps('Export', [{ do: 'click', target: 'Export', expect: 'the sheet shows' }], window.state(), deps(window, ask));
|
|
495
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'verified', used: { key: 'w/1', label: 'button\u001fExport', way: 0 } });
|
|
496
|
+
expect(report.outcomes[0]?.recalled).toBeUndefined();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('asks Jev again when the remembered element no longer does it, and says the memory is stale', async () => {
|
|
500
|
+
const window = app([button(1, 'Export'), button(2, 'Old')], (action, elements) => {
|
|
501
|
+
if (action.action === 'click' && action.element_index === 1) elements.push(button(3, 'Sheet'));
|
|
502
|
+
});
|
|
503
|
+
const { ask, requests } = jev((state, id) => (id === 'target' ? pick(1) : id === 'expected' ? yes(state.elements.includes('"Sheet"') ? 0.9 : 0) : undefined));
|
|
504
|
+
const known: RunDeps['known'] = (_step, tree) => ({ element: tree.elements[2] as AppElement, way: 0 });
|
|
505
|
+
const report = await runSteps('Export', [{ do: 'click', target: 'the button that exports', expect: 'the sheet shows' }], window.state(), { ...deps(window, ask), known });
|
|
506
|
+
expect(window.acted.map((action) => (action as { element_index?: number }).element_index)).toEqual([2, 1]);
|
|
507
|
+
expect(requests.map((request) => request.ids)).toEqual([['already'], ['expected'], ['target', 'already'], ['expected']]);
|
|
508
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'verified', stale: true, used: { key: 'w/1' } });
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe('what a step showed last time', () => {
|
|
513
|
+
const sheet = 'button\u001fSheet';
|
|
514
|
+
const opens = () => app([button(1, 'Export')], (action, elements) => { if (action.action === 'click') elements.push(button(2, 'Sheet')); });
|
|
515
|
+
const step = { do: 'click' as const, target: 'Export', expect: 'the sheet shows' };
|
|
516
|
+
|
|
517
|
+
it('is reported with a verified step', async () => {
|
|
518
|
+
const window = opens();
|
|
519
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : id === 'expected' ? yes(0.9) : undefined));
|
|
520
|
+
const report = await runSteps('Export', [step], window.state(), deps(window, ask));
|
|
521
|
+
expect(report.outcomes[0]?.used).toEqual({ key: 'w/1', label: 'button\u001fExport', way: 0, effect: [sheet] });
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('checks a remembered step without Jev when the same shows again', async () => {
|
|
525
|
+
const window = opens();
|
|
526
|
+
const { ask, requests } = jev(() => undefined);
|
|
527
|
+
const known: RunDeps['known'] = (_step, tree) => ({ element: tree.elements[1] as AppElement, way: 0, effect: [sheet] });
|
|
528
|
+
const report = await runSteps('Export', [step], window.state(), { ...deps(window, ask), known });
|
|
529
|
+
expect(requests).toEqual([]);
|
|
530
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'verified', recalled: true, used: { key: 'w/1', effect: [sheet] } });
|
|
531
|
+
expect(report.asks).toBe(0);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('tells the action what to wait for, so it can return the moment that shows', async () => {
|
|
535
|
+
const window = opens();
|
|
536
|
+
const { ask } = jev(() => undefined);
|
|
537
|
+
const known: RunDeps['known'] = (_step, tree) => ({ element: tree.elements[1] as AppElement, way: 0, effect: [sheet] });
|
|
538
|
+
const waited: Array<readonly string[] | undefined> = [];
|
|
539
|
+
const act: RunDeps['act'] = (action, { until }) => { waited.push(until); return window.act(action); };
|
|
540
|
+
await runSteps('Export', [step], window.state(), { ...deps(window, ask), known, act });
|
|
541
|
+
expect(waited).toEqual([[sheet]]);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('skips a remembered step when what it showed is already there and its element is the chosen one', async () => {
|
|
545
|
+
const window = app([button(1, 'Export', { role: 'row', states: ['selected'] }), button(2, 'Sheet')]);
|
|
546
|
+
const { ask, requests } = jev(() => undefined);
|
|
547
|
+
const known: RunDeps['known'] = (_step, tree) => ({ element: tree.elements[1] as AppElement, way: 0, effect: [sheet] });
|
|
548
|
+
const report = await runSteps('Export', [step], window.state(), { ...deps(window, ask), known });
|
|
549
|
+
expect(window.acted).toEqual([]);
|
|
550
|
+
expect(requests).toEqual([]);
|
|
551
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'skipped', recalled: true });
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// "New tab" on a start page: the window already looks like the result, and the step still has to be done.
|
|
555
|
+
it('does not take a step for done by the look of the window when its element is not a chosen one', async () => {
|
|
556
|
+
const window = app([button(1, 'Export'), button(2, 'Sheet')], (action, elements) => { if (action.action === 'click') elements.push(button(3, 'Second sheet')); });
|
|
557
|
+
const { ask, requests } = jev((_state, id) => (id === 'expected' ? yes(0.9) : undefined));
|
|
558
|
+
const known: RunDeps['known'] = (_step, tree) => ({ element: tree.elements[1] as AppElement, way: 0, effect: [sheet] });
|
|
559
|
+
const report = await runSteps('Export', [step], window.state(), { ...deps(window, ask), known });
|
|
560
|
+
expect(window.acted).toHaveLength(1);
|
|
561
|
+
expect(requests.map((request) => request.ids)).toEqual([['already'], ['expected']]);
|
|
562
|
+
expect(report.outcomes[0]?.status).toBe('verified');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('asks Jev when the window shows something else this time', async () => {
|
|
566
|
+
const window = app([button(1, 'Export')], (action, elements) => { if (action.action === 'click') elements.push(button(2, 'Error')); });
|
|
567
|
+
const { ask, requests } = jev((_state, id) => (id === 'expected' ? yes(0.9) : undefined));
|
|
568
|
+
const known: RunDeps['known'] = (_step, tree) => ({ element: tree.elements[1] as AppElement, way: 0, effect: [sheet] });
|
|
569
|
+
const report = await runSteps('Export', [step], window.state(), { ...deps(window, ask), known });
|
|
570
|
+
expect(requests.map((request) => request.ids)).toEqual([['expected']]);
|
|
571
|
+
expect(report.outcomes[0]?.status).toBe('verified');
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
describe('what a step shows differently every time', () => {
|
|
576
|
+
// A calculator key: what appears is the number on the display, never the same twice.
|
|
577
|
+
const sheet = 'button\u001fSheet';
|
|
578
|
+
const step = { do: 'click' as const, target: 'Export', expect: 'the sheet shows' };
|
|
579
|
+
const changing = () => app([button(1, 'Export')], (action, elements) => { if (action.action === 'click') elements.push(button(2, 'Error')); });
|
|
580
|
+
const judged = () => jev((_state, id) => (id === 'expected' ? yes(0.9) : undefined));
|
|
581
|
+
|
|
582
|
+
it('is forgotten as its sign: the lesson then says the step leaves none', async () => {
|
|
583
|
+
const window = changing();
|
|
584
|
+
const known: RunDeps['known'] = (_step, tree) => ({ element: tree.elements[1] as AppElement, way: 0, effect: [sheet] });
|
|
585
|
+
const report = await runSteps('Export', [step], window.state(), { ...deps(window, judged().ask), known });
|
|
586
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'verified', used: { key: 'w/1', effect: [] } });
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('keeps the part of the sign that did show again', async () => {
|
|
590
|
+
const window = app([button(1, 'Export')], (action, elements) => { if (action.action === 'click') elements.push(button(2, 'Sheet'), button(3, 'Error')); });
|
|
591
|
+
const known: RunDeps['known'] = (_step, tree) => ({ element: tree.elements[1] as AppElement, way: 0, effect: [sheet, 'button\u001f42'] });
|
|
592
|
+
const report = await runSteps('Export', [step], window.state(), { ...deps(window, judged().ask), known });
|
|
593
|
+
expect(report.outcomes[0]?.used?.effect).toEqual([sheet]);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('is not waited for once the lesson says so, and stays that way', async () => {
|
|
597
|
+
const window = changing();
|
|
598
|
+
const { ask, requests } = judged();
|
|
599
|
+
const known: RunDeps['known'] = (_step, tree) => ({ element: tree.elements[1] as AppElement, way: 0, effect: [] });
|
|
600
|
+
const waited: Array<readonly string[] | undefined> = [];
|
|
601
|
+
const act: RunDeps['act'] = (action, { until }) => { waited.push(until); return window.act(action); };
|
|
602
|
+
const report = await runSteps('Export', [step], window.state(), { ...deps(window, ask), known, act });
|
|
603
|
+
expect(waited).toEqual([undefined]);
|
|
604
|
+
expect(requests.map((request) => request.ids)).toEqual([['already'], ['expected']]);
|
|
605
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'verified', used: { effect: [] } });
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
describe('what typing showed', () => {
|
|
610
|
+
it('is not kept as a step\'s effect: the text differs every time', async () => {
|
|
611
|
+
const window = app([button(1, 'Search', { role: 'text field', value: '' })], (action, elements) => {
|
|
612
|
+
if (action.action === 'type_text') elements.push(button(2, action.text, { role: 'text' }));
|
|
613
|
+
});
|
|
614
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : id === 'expected' ? yes(0.9) : undefined));
|
|
615
|
+
const report = await runSteps('Find', [{ do: 'type', target: 'Search', text: 'Kopernik', expect: 'the field holds Kopernik' }], window.state(), deps(window, ask));
|
|
616
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'verified', used: { key: 'w/1', way: 0 } });
|
|
617
|
+
expect(report.outcomes[0]?.used?.effect).toBeUndefined();
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
describe('a step that says nothing of its result', () => {
|
|
622
|
+
const window = () => app([button(1, 'Search', { role: 'text field', value: '' })], (action, elements) => {
|
|
623
|
+
if (action.action === 'type_text') (elements[0] as AppElement).value = action.text;
|
|
624
|
+
if (action.action === 'press_key') elements.push(button(2, 'Results', { role: 'text' }));
|
|
625
|
+
});
|
|
626
|
+
const steps: RunStep[] = [{ do: 'type', target: 'Search', text: 'Kopernik' }, { do: 'key', key: 'Return', expect: 'results show' }];
|
|
627
|
+
|
|
628
|
+
it('is learned once a later step of the run was seen to work', async () => {
|
|
629
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : id === 'expected' ? yes(0.9) : undefined));
|
|
630
|
+
const opened = window();
|
|
631
|
+
const report = await runSteps('Find', steps, opened.state(), deps(opened, ask));
|
|
632
|
+
expect(report.outcomes.map((outcome) => outcome.status)).toEqual(['done', 'verified']);
|
|
633
|
+
expect(report.outcomes[0]?.used).toMatchObject({ key: 'w/1', way: 0 });
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('is not learned when nothing after it was seen to work', async () => {
|
|
637
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : undefined));
|
|
638
|
+
const opened = window();
|
|
639
|
+
const report = await runSteps('Find', [steps[0] as RunStep, { do: 'key', key: 'Return' }], opened.state(), deps(opened, ask));
|
|
640
|
+
expect(report.outcomes[0]?.used).toBeUndefined();
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
describe('a target worded differently than the lesson', () => {
|
|
645
|
+
const sheet = 'button\u001fSheet';
|
|
646
|
+
const opens = () => app([button(1, 'Export'), button(2, 'Share')], (action, elements) => { if (action.action === 'click' && action.element_index === 1) elements.push(button(3, 'Sheet')); });
|
|
647
|
+
const steps: RunStep[] = [{ do: 'click', target: 'the Export control up top', expect: 'the sheet shows' }];
|
|
648
|
+
const guessed: RunDeps['guess'] = (_step, tree) => ({ element: tree.elements[1] as AppElement, way: 0, effect: [sheet] });
|
|
649
|
+
|
|
650
|
+
it('asks Jev once whether the guessed lessons fit, then runs from memory', async () => {
|
|
651
|
+
const window = opens();
|
|
652
|
+
const { ask, requests } = jev((_state, id) => (id === 'same_0' ? yes(0.95) : undefined));
|
|
653
|
+
const report = await runSteps('Export', steps, window.state(), { ...deps(window, ask), guess: guessed });
|
|
654
|
+
expect(requests.map((request) => request.ids)).toEqual([['same_0']]);
|
|
655
|
+
expect(window.acted).toEqual([{ action: 'click', element_index: 1, mouse_button: 'left', click_count: 1 }]);
|
|
656
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'verified', recalled: true, used: { key: 'w/1' } });
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('finds the element the usual way when Jev says the guess is another element', async () => {
|
|
660
|
+
const window = opens();
|
|
661
|
+
const { ask, requests } = jev((_state, id) => (id === 'same_0' ? yes(0.1) : id === 'target' ? pick(1) : id === 'expected' ? yes(0.9) : undefined));
|
|
662
|
+
const report = await runSteps('Export', steps, window.state(), { ...deps(window, ask), guess: guessed });
|
|
663
|
+
expect(requests.map((request) => request.ids)).toEqual([['same_0'], ['target', 'already'], ['expected']]);
|
|
664
|
+
expect(report.outcomes[0]?.recalled).toBeUndefined();
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
describe('a key with an expected result', () => {
|
|
669
|
+
const results = 'text\u001fResults';
|
|
670
|
+
const opens = () => app([button(1, 'Search')], (action, elements) => { if (action.action === 'press_key') elements.push(button(2, 'Results', { role: 'text' })); });
|
|
671
|
+
const step: RunStep = { do: 'key', key: 'Return', expect: 'results show' };
|
|
672
|
+
|
|
673
|
+
it('reports what it made appear, so it can be remembered', async () => {
|
|
674
|
+
const window = opens();
|
|
675
|
+
const { ask } = jev((_state, id) => (id === 'expected' ? yes(0.9) : undefined));
|
|
676
|
+
const report = await runSteps('Find', [step], window.state(), deps(window, ask));
|
|
677
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'verified', used: { way: 0, effect: [results] } });
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('is checked without Jev when the same shows again', async () => {
|
|
681
|
+
const window = opens();
|
|
682
|
+
const { ask, requests } = jev(() => undefined);
|
|
683
|
+
const report = await runSteps('Find', [step], window.state(), { ...deps(window, ask), known: () => ({ way: 0, effect: [results] }) });
|
|
684
|
+
expect(requests).toEqual([]);
|
|
685
|
+
expect(window.acted).toHaveLength(1);
|
|
686
|
+
expect(report.outcomes[0]).toMatchObject({ status: 'verified', recalled: true });
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
describe('describeRun', () => {
|
|
691
|
+
it('says what was done to which element and how long it took', async () => {
|
|
692
|
+
const window = app([button(1, 'Export')], (_action, elements) => { elements.push(button(2, 'Dialog')); });
|
|
693
|
+
const { ask } = jev((_state, id) => (id === 'target' ? pick(1) : id === 'expected' ? yes(0.9) : undefined));
|
|
694
|
+
const steps: RunStep[] = [{ do: 'click', target: 'Export', expect: 'a dialog is open' }, { do: 'key', key: 'Return' }];
|
|
695
|
+
const text = describeRun(await runSteps('Export', steps, window.state(), deps(window, ask)), steps);
|
|
696
|
+
expect(text).toMatch(/^computer_run: 2 of 2 steps done in \d+(\.\d)? s/);
|
|
697
|
+
expect(text).toContain('1. verified — click "Export" → [1] button "Export"');
|
|
698
|
+
expect(text).toContain('2. done — key Return');
|
|
699
|
+
});
|
|
700
|
+
});
|