@scenetest/scenes 0.2.0 → 0.4.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.
Files changed (63) hide show
  1. package/dist/__tests__/dsl.test.js +20 -0
  2. package/dist/__tests__/dsl.test.js.map +1 -1
  3. package/dist/__tests__/keyboard.test.d.ts +2 -0
  4. package/dist/__tests__/keyboard.test.d.ts.map +1 -0
  5. package/dist/__tests__/keyboard.test.js +332 -0
  6. package/dist/__tests__/keyboard.test.js.map +1 -0
  7. package/dist/__tests__/markdown-scene.test.js +85 -5
  8. package/dist/__tests__/markdown-scene.test.js.map +1 -1
  9. package/dist/__tests__/reactive.test.js +259 -6
  10. package/dist/__tests__/reactive.test.js.map +1 -1
  11. package/dist/__tests__/runner.test.d.ts +2 -0
  12. package/dist/__tests__/runner.test.d.ts.map +1 -0
  13. package/dist/__tests__/runner.test.js +182 -0
  14. package/dist/__tests__/runner.test.js.map +1 -0
  15. package/dist/actor.d.ts +21 -4
  16. package/dist/actor.d.ts.map +1 -1
  17. package/dist/actor.js +176 -12
  18. package/dist/actor.js.map +1 -1
  19. package/dist/builtin-macros.d.ts +87 -0
  20. package/dist/builtin-macros.d.ts.map +1 -0
  21. package/dist/builtin-macros.js +154 -0
  22. package/dist/builtin-macros.js.map +1 -0
  23. package/dist/cli.js +12 -0
  24. package/dist/cli.js.map +1 -1
  25. package/dist/devices.d.ts +6 -0
  26. package/dist/devices.d.ts.map +1 -1
  27. package/dist/devices.js +13 -0
  28. package/dist/devices.js.map +1 -1
  29. package/dist/dsl.d.ts +4 -0
  30. package/dist/dsl.d.ts.map +1 -1
  31. package/dist/dsl.js +18 -1
  32. package/dist/dsl.js.map +1 -1
  33. package/dist/index.d.ts +5 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +5 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/keyboard.d.ts +168 -0
  38. package/dist/keyboard.d.ts.map +1 -0
  39. package/dist/keyboard.js +370 -0
  40. package/dist/keyboard.js.map +1 -0
  41. package/dist/markdown-scene.d.ts +4 -2
  42. package/dist/markdown-scene.d.ts.map +1 -1
  43. package/dist/markdown-scene.js +21 -9
  44. package/dist/markdown-scene.js.map +1 -1
  45. package/dist/reactive.d.ts +25 -2
  46. package/dist/reactive.d.ts.map +1 -1
  47. package/dist/reactive.js +163 -16
  48. package/dist/reactive.js.map +1 -1
  49. package/dist/runner.d.ts +11 -5
  50. package/dist/runner.d.ts.map +1 -1
  51. package/dist/runner.js +141 -35
  52. package/dist/runner.js.map +1 -1
  53. package/dist/swarm.d.ts +1 -1
  54. package/dist/swarm.d.ts.map +1 -1
  55. package/dist/swarm.js +8 -5
  56. package/dist/swarm.js.map +1 -1
  57. package/dist/team-manager.d.ts +48 -3
  58. package/dist/team-manager.d.ts.map +1 -1
  59. package/dist/team-manager.js +150 -15
  60. package/dist/team-manager.js.map +1 -1
  61. package/dist/types.d.ts +133 -2
  62. package/dist/types.d.ts.map +1 -1
  63. package/package.json +3 -3
@@ -0,0 +1,182 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { runCleanup, runSetup } from '../runner.js';
3
+ // ---------------------------------------------------------------------------
4
+ // Helpers
5
+ // ---------------------------------------------------------------------------
6
+ function makeScene(overrides = {}) {
7
+ return {
8
+ name: 'test scene',
9
+ file: '/test/scene.spec.md',
10
+ fn: async () => { },
11
+ cleanup: [],
12
+ setup: [],
13
+ ...overrides,
14
+ };
15
+ }
16
+ const team = {
17
+ learner: { key: 'learner-123', username: 'alice', email: 'alice@test.com' },
18
+ friend: { key: 'friend-456', username: 'bob', email: 'bob@test.com' },
19
+ };
20
+ const teamMeta = {
21
+ name: 'Kannada Team',
22
+ tags: { lang: 'kan', region: 'asia' },
23
+ };
24
+ const testStart = '2026-04-02T10:00:00.000Z';
25
+ // ---------------------------------------------------------------------------
26
+ // runCleanup
27
+ // ---------------------------------------------------------------------------
28
+ describe('runCleanup', () => {
29
+ it('does nothing when cleanup is empty', async () => {
30
+ const scene = makeScene();
31
+ const server = { supabase: { ran: false } };
32
+ await runCleanup(scene, team, teamMeta, server, testStart);
33
+ expect(server.supabase.ran).toBe(false);
34
+ });
35
+ it('evaluates a single cleanup expression with server in scope', async () => {
36
+ const calls = [];
37
+ const server = {
38
+ db: {
39
+ delete: (table) => { calls.push(`delete:${table}`); return Promise.resolve(); },
40
+ },
41
+ };
42
+ const scene = makeScene({ cleanup: ["db.delete('user_deck')"] });
43
+ await runCleanup(scene, team, teamMeta, server, testStart);
44
+ expect(calls).toEqual(["delete:user_deck"]);
45
+ });
46
+ it('runs all expressions when multiple cleanup: lines specified', async () => {
47
+ const calls = [];
48
+ const server = {
49
+ db: {
50
+ delete: (table) => { calls.push(`delete:${table}`); return Promise.resolve(); },
51
+ },
52
+ };
53
+ const scene = makeScene({
54
+ cleanup: ["db.delete('comments')", "db.delete('notifications')"],
55
+ });
56
+ await runCleanup(scene, team, teamMeta, server, testStart);
57
+ expect(calls).toEqual(['delete:comments', 'delete:notifications']);
58
+ });
59
+ it('interpolates [role.field] tokens', async () => {
60
+ let capturedKey = '';
61
+ const server = {
62
+ db: {
63
+ deleteByKey: (key) => { capturedKey = key; return Promise.resolve(); },
64
+ },
65
+ };
66
+ const scene = makeScene({ cleanup: ["db.deleteByKey('[learner.key]')"] });
67
+ await runCleanup(scene, team, teamMeta, server, testStart);
68
+ expect(capturedKey).toBe('learner-123');
69
+ });
70
+ it('interpolates [team.field] tokens from team metadata tags', async () => {
71
+ let capturedLang = '';
72
+ const server = {
73
+ db: {
74
+ deleteByLang: (lang) => { capturedLang = lang; return Promise.resolve(); },
75
+ },
76
+ };
77
+ const scene = makeScene({ cleanup: ["db.deleteByLang('[team.lang]')"] });
78
+ await runCleanup(scene, team, teamMeta, server, testStart);
79
+ expect(capturedLang).toBe('kan');
80
+ });
81
+ it('interpolates [testStart] with the ISO timestamp', async () => {
82
+ let capturedTs = '';
83
+ const server = {
84
+ db: {
85
+ deleteAfter: (ts) => { capturedTs = ts; return Promise.resolve(); },
86
+ },
87
+ };
88
+ const scene = makeScene({ cleanup: ["db.deleteAfter('[testStart]')"] });
89
+ await runCleanup(scene, team, teamMeta, server, testStart);
90
+ expect(capturedTs).toBe(testStart);
91
+ });
92
+ it('logs a warning and skips when no server config', async () => {
93
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
94
+ const scene = makeScene({ cleanup: ["db.delete('x')"] });
95
+ await runCleanup(scene, team, teamMeta, undefined, testStart);
96
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('no server config'));
97
+ warnSpy.mockRestore();
98
+ });
99
+ it('logs a warning but does not throw when expression errors', async () => {
100
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
101
+ const server = {
102
+ db: {
103
+ badCall: () => { throw new Error('db connection refused'); },
104
+ },
105
+ };
106
+ const scene = makeScene({ cleanup: ['db.badCall()'] });
107
+ await expect(runCleanup(scene, team, teamMeta, server, testStart)).resolves.toBeUndefined();
108
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('db connection refused'));
109
+ warnSpy.mockRestore();
110
+ });
111
+ it('throws interpolation error and logs warning for unknown role', async () => {
112
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
113
+ const server = { db: {} };
114
+ const scene = makeScene({ cleanup: ["db.fn('[ghost.key]')"] });
115
+ await runCleanup(scene, team, teamMeta, server, testStart);
116
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('ghost'));
117
+ warnSpy.mockRestore();
118
+ });
119
+ it('throws interpolation error and logs warning for unknown team tag', async () => {
120
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
121
+ const server = { db: {} };
122
+ const scene = makeScene({ cleanup: ["db.fn('[team.notexist]')"] });
123
+ await runCleanup(scene, team, teamMeta, server, testStart);
124
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('notexist'));
125
+ warnSpy.mockRestore();
126
+ });
127
+ });
128
+ // ---------------------------------------------------------------------------
129
+ // runSetup
130
+ // ---------------------------------------------------------------------------
131
+ describe('runSetup', () => {
132
+ it('does nothing when setup is empty', async () => {
133
+ const calls = [];
134
+ const server = { db: { update: (t) => { calls.push(t); return Promise.resolve(); } } };
135
+ const scene = makeScene();
136
+ await runSetup(scene, team, teamMeta, server, testStart);
137
+ expect(calls).toHaveLength(0);
138
+ });
139
+ it('evaluates each setup expression in order', async () => {
140
+ const calls = [];
141
+ const server = {
142
+ db: {
143
+ update: (t) => { calls.push(`update:${t}`); return Promise.resolve(); },
144
+ },
145
+ };
146
+ const scene = makeScene({
147
+ setup: ["db.update('step1')", "db.update('step2')"],
148
+ });
149
+ await runSetup(scene, team, teamMeta, server, testStart);
150
+ expect(calls).toEqual(['update:step1', 'update:step2']);
151
+ });
152
+ it('interpolates [role.field] and [team.field] in setup expressions', async () => {
153
+ const captured = [];
154
+ const server = {
155
+ db: {
156
+ seed: (key, lang) => {
157
+ captured.push(`${key}:${lang}`);
158
+ return Promise.resolve();
159
+ },
160
+ },
161
+ };
162
+ const scene = makeScene({ setup: ["db.seed('[learner.key]', '[team.lang]')"] });
163
+ await runSetup(scene, team, teamMeta, server, testStart);
164
+ expect(captured).toEqual(['learner-123:kan']);
165
+ });
166
+ it('logs a warning but does not throw on error, continues remaining expressions', async () => {
167
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
168
+ const calls = [];
169
+ const server = {
170
+ db: {
171
+ bad: () => { throw new Error('setup failed'); },
172
+ ok: () => { calls.push('ok'); return Promise.resolve(); },
173
+ },
174
+ };
175
+ const scene = makeScene({ setup: ['db.bad()', 'db.ok()'] });
176
+ await runSetup(scene, team, teamMeta, server, testStart);
177
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('setup failed'));
178
+ expect(calls).toEqual(['ok']);
179
+ warnSpy.mockRestore();
180
+ });
181
+ });
182
+ //# sourceMappingURL=runner.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.test.js","sourceRoot":"","sources":["../../src/__tests__/runner.test.ts"],"names":[],"mappings":"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"}
package/dist/actor.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Page, BrowserContext } from 'playwright';
2
- import type { ActorConfig, SequentialActorHandle, ActionChain, AssertionResult, TimelineEntry, ScriptWarning, Selector } from './types.js';
2
+ import type { ActorConfig, SequentialActorHandle, ActionChain, AssertionResult, TimelineEntry, ScriptWarning, Selector, PageFactory } from './types.js';
3
+ import type { NavigationMode } from './keyboard.js';
3
4
  import { MessageBus } from './message-bus.js';
4
5
  /**
5
6
  * Registered watcher for conditional handling
@@ -29,17 +30,28 @@ export declare class SequentialActorHandleImpl implements SequentialActorHandle
29
30
  private actionTimeout;
30
31
  private warnAfter;
31
32
  readonly role: string;
32
- readonly page: Page;
33
- readonly context: BrowserContext;
33
+ private _page;
34
+ private _context;
34
35
  readonly assertions: AssertionResult[];
36
+ readonly navigationMode: NavigationMode;
37
+ readonly fuzzyFingers: boolean;
35
38
  private watchers;
36
39
  private warningTriggers;
40
+ private _pageFactory;
37
41
  readonly key: string;
38
42
  readonly username?: string;
39
43
  readonly email?: string;
40
44
  readonly password?: string;
41
45
  [key: string]: unknown;
42
- constructor(role: string, config: ActorConfig, page: Page, context: BrowserContext, bus: MessageBus, timeline: TimelineEntry[], warnings: ScriptWarning[], actionTimeout: number, warnAfter: number);
46
+ constructor(role: string, config: ActorConfig, page: Page, context: BrowserContext, bus: MessageBus, timeline: TimelineEntry[], warnings: ScriptWarning[], actionTimeout: number, warnAfter: number, navigationMode?: NavigationMode, fuzzyFingers?: boolean, pageFactory?: PageFactory | null);
47
+ /** Playwright page for this actor */
48
+ get page(): Page;
49
+ /** Playwright browser context for this actor */
50
+ get context(): BrowserContext;
51
+ /** Get the page factory (used by ActionChainImpl for switchDevice) */
52
+ getPageFactory(): PageFactory | null;
53
+ /** Update page and context after a device switch */
54
+ _switchPage(page: Page, context: BrowserContext): void;
43
55
  /**
44
56
  * Get registered watchers (used by ActionChainImpl)
45
57
  */
@@ -55,6 +67,10 @@ export declare class SequentialActorHandleImpl implements SequentialActorHandle
55
67
  clearWatchers(): void;
56
68
  private createChain;
57
69
  openTo(url: string): ActionChain;
70
+ reload(): ActionChain;
71
+ goBack(): ActionChain;
72
+ goForward(): ActionChain;
73
+ switchDevice(device?: string): ActionChain;
58
74
  see(selector: Selector): ActionChain;
59
75
  seeInView(selector: Selector): ActionChain;
60
76
  notSee(selector: Selector): ActionChain;
@@ -67,6 +83,7 @@ export declare class SequentialActorHandleImpl implements SequentialActorHandle
67
83
  wait(ms: number): ActionChain;
68
84
  emit(message: string): ActionChain;
69
85
  waitFor(message: string): ActionChain;
86
+ pressKey(key: string): ActionChain;
70
87
  do(fn: (page: Page) => Promise<void>): ActionChain;
71
88
  scrollToBottom(): ActionChain;
72
89
  up(selector?: Selector): ActionChain;
@@ -1 +1 @@
1
- {"version":3,"file":"actor.d.ts","sourceRoot":"","sources":["../src/actor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"actor.d.ts","sourceRoot":"","sources":["../src/actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAW,MAAM,YAAY,CAAA;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACvJ,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAc7C;;GAEG;AACH,UAAU,OAAO;IACf,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,SAAS,EAAE,OAAO,CAAA;CACnB;AAED;;GAEG;AACH,UAAU,cAAc;IACtB,QAAQ,EAAE,QAAQ,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,OAAO,CAAA;CACnB;AA6kBD;;;GAGG;AACH,qBAAa,yBAA0B,YAAW,qBAAqB;aA0BnD,MAAM,EAAE,WAAW;IAGnC,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,SAAS;IAhCnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,QAAQ,CAAgB;IAChC,QAAQ,CAAC,UAAU,EAAE,eAAe,EAAE,CAAK;IAC3C,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAA;IACvC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;IAG9B,OAAO,CAAC,QAAQ,CAAgB;IAGhC,OAAO,CAAC,eAAe,CAAuB;IAG9C,OAAO,CAAC,YAAY,CAAoB;IAGxC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;gBAGpB,IAAI,EAAE,MAAM,EACI,MAAM,EAAE,WAAW,EACnC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,cAAc,EACf,GAAG,EAAE,UAAU,EACf,QAAQ,EAAE,aAAa,EAAE,EACzB,QAAQ,EAAE,aAAa,EAAE,EACzB,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACzB,cAAc,GAAE,cAA0B,EAC1C,YAAY,GAAE,OAAe,EAC7B,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI;IAoBlC,qCAAqC;IACrC,IAAI,IAAI,IAAI,IAAI,CAEf;IAED,gDAAgD;IAChD,IAAI,OAAO,IAAI,cAAc,CAE5B;IAED,sEAAsE;IACtE,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC,oDAAoD;IACpD,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI;IAKtD;;OAEG;IACH,WAAW,IAAI,OAAO,EAAE;IAIxB;;OAEG;IACH,kBAAkB,IAAI,cAAc,EAAE;IAItC;;;OAGG;IACH,aAAa,IAAI,IAAI;IAIrB,OAAO,CAAC,WAAW;IAInB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW;IAIhC,MAAM,IAAI,WAAW;IAIrB,MAAM,IAAI,WAAW;IAIrB,SAAS,IAAI,WAAW;IAIxB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW;IAI1C,GAAG,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW;IAIpC,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW;IAI1C,MAAM,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW;IAIvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW;IAIlC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW;IAIzC,KAAK,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,WAAW;IAIvC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW;IAIxD,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW;IAItC,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW;IAItD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW;IAI7B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW;IAIlC,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW;IAIrC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW;IAIlC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,WAAW;IAIlD,cAAc,IAAI,WAAW;IAI7B,EAAE,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,WAAW;IAIpC,IAAI,IAAI,WAAW;IAInB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW;IAI9B;;;;;;;;;;;OAWG;IACH,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAQ3D;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;CAOlD"}
package/dist/actor.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { tabToElement, pressEnter, pressSpace, clearAndType, keyboardSelectOption, fuzzyFingerClick, fuzzyFingerFill, fuzzyFingerCheck } from './keyboard.js';
1
2
  import { resolveSelector } from './selectors.js';
2
3
  import { parseDslLines, parseAction, applyDslAction } from './dsl.js';
4
+ import { findDevice } from './devices.js';
3
5
  /**
4
6
  * Format a Selector for display in logs/errors
5
7
  */
@@ -19,6 +21,8 @@ class ActionChainImpl {
19
21
  warnings;
20
22
  actionTimeout;
21
23
  warnAfter;
24
+ navigationMode;
25
+ fuzzyFingers;
22
26
  actions = [];
23
27
  // Current scope - starts as page, changes with see()
24
28
  currentScope;
@@ -28,7 +32,7 @@ class ActionChainImpl {
28
32
  scopeSetUrl = '';
29
33
  // Per-entry URLs matching scopeStack (kept in sync)
30
34
  scopeStackUrls = [];
31
- constructor(actor, page, bus, timeline, warnings, actionTimeout, warnAfter) {
35
+ constructor(actor, page, bus, timeline, warnings, actionTimeout, warnAfter, navigationMode = 'pointer', fuzzyFingers = false) {
32
36
  this.actor = actor;
33
37
  this.page = page;
34
38
  this.bus = bus;
@@ -36,8 +40,23 @@ class ActionChainImpl {
36
40
  this.warnings = warnings;
37
41
  this.actionTimeout = actionTimeout;
38
42
  this.warnAfter = warnAfter;
43
+ this.navigationMode = navigationMode;
44
+ this.fuzzyFingers = fuzzyFingers;
39
45
  this.currentScope = page;
40
46
  }
47
+ /**
48
+ * Whether this chain uses keyboard navigation.
49
+ */
50
+ get isKeyboard() {
51
+ return this.navigationMode === 'keyboard';
52
+ }
53
+ /**
54
+ * Whether fuzzy-finger touch behavior is enabled for pointer mode.
55
+ * Fuzzy-finger simulates imprecise human touch: miss → pause → correct click.
56
+ */
57
+ get useFuzzyFingers() {
58
+ return this.navigationMode === 'pointer' && this.fuzzyFingers;
59
+ }
41
60
  addAction(name, target, execute) {
42
61
  this.actions.push({ name, target, execute });
43
62
  return this;
@@ -120,6 +139,53 @@ class ActionChainImpl {
120
139
  this.scopeStackUrls = [];
121
140
  });
122
141
  }
142
+ reload() {
143
+ return this.addAction('reload', undefined, async () => {
144
+ await this.page.reload({ timeout: this.actionTimeout });
145
+ this.currentScope = this.page;
146
+ this.scopeStack = [];
147
+ this.scopeSetUrl = '';
148
+ this.scopeStackUrls = [];
149
+ });
150
+ }
151
+ goBack() {
152
+ return this.addAction('goBack', undefined, async () => {
153
+ await this.page.goBack({ timeout: this.actionTimeout });
154
+ this.currentScope = this.page;
155
+ this.scopeStack = [];
156
+ this.scopeSetUrl = '';
157
+ this.scopeStackUrls = [];
158
+ });
159
+ }
160
+ goForward() {
161
+ return this.addAction('goForward', undefined, async () => {
162
+ await this.page.goForward({ timeout: this.actionTimeout });
163
+ this.currentScope = this.page;
164
+ this.scopeStack = [];
165
+ this.scopeSetUrl = '';
166
+ this.scopeStackUrls = [];
167
+ });
168
+ }
169
+ switchDevice(device) {
170
+ return this.addAction('switchDevice', device ?? '(next)', async () => {
171
+ const factory = this.actor.getPageFactory();
172
+ if (!factory) {
173
+ throw new Error('switchDevice requires the scene runner (page factory not available)');
174
+ }
175
+ const profile = device ? findDevice(device) : null;
176
+ // Factory closes old context, creates new one with assertion wiring
177
+ const { page, context } = await factory(profile);
178
+ // Update actor's backing references
179
+ this.actor._switchPage(page, context);
180
+ // Update chain's local page reference
181
+ this.page = page;
182
+ // Reset scope to new page root
183
+ this.currentScope = this.page;
184
+ this.scopeStack = [];
185
+ this.scopeSetUrl = '';
186
+ this.scopeStackUrls = [];
187
+ });
188
+ }
123
189
  see(selector) {
124
190
  const target = formatSelector(selector);
125
191
  return this.addAction('see', target, async () => {
@@ -177,31 +243,85 @@ class ActionChainImpl {
177
243
  if (scope === this.page) {
178
244
  throw new Error('click with no selector requires a scope (use see() first)');
179
245
  }
180
- await scope.click({ timeout: this.actionTimeout });
246
+ if (this.isKeyboard) {
247
+ await tabToElement(this.page, scope, { timeout: this.actionTimeout });
248
+ await pressEnter(this.page);
249
+ }
250
+ else if (this.useFuzzyFingers) {
251
+ await fuzzyFingerClick(this.page, scope, this.actionTimeout, '(scope)');
252
+ }
253
+ else {
254
+ await scope.click({ timeout: this.actionTimeout });
255
+ }
181
256
  });
182
257
  }
183
258
  const target = formatSelector(selector);
184
259
  return this.addAction('click', target, async () => {
185
- await resolveSelector(this.getScope(), selector).click({ timeout: this.actionTimeout });
260
+ const locator = resolveSelector(this.getScope(), selector);
261
+ if (this.isKeyboard) {
262
+ await locator.waitFor({ state: 'visible', timeout: this.actionTimeout });
263
+ await tabToElement(this.page, locator, { timeout: this.actionTimeout });
264
+ await pressEnter(this.page);
265
+ }
266
+ else if (this.useFuzzyFingers) {
267
+ await fuzzyFingerClick(this.page, locator, this.actionTimeout, selector);
268
+ }
269
+ else {
270
+ await locator.click({ timeout: this.actionTimeout });
271
+ }
186
272
  });
187
273
  }
188
274
  typeInto(selector, value) {
189
275
  const target = `${formatSelector(selector)}=${value}`;
190
276
  return this.addAction('typeInto', target, async () => {
191
- await resolveSelector(this.getScope(), selector).fill(value, { timeout: this.actionTimeout });
277
+ const locator = resolveSelector(this.getScope(), selector);
278
+ if (this.isKeyboard) {
279
+ await locator.waitFor({ state: 'visible', timeout: this.actionTimeout });
280
+ await tabToElement(this.page, locator, { timeout: this.actionTimeout });
281
+ await clearAndType(this.page, value);
282
+ }
283
+ else if (this.useFuzzyFingers) {
284
+ await fuzzyFingerFill(this.page, locator, value, this.actionTimeout, selector);
285
+ }
286
+ else {
287
+ await locator.fill(value, { timeout: this.actionTimeout });
288
+ }
192
289
  // typeInto stays in current scope
193
290
  });
194
291
  }
195
292
  check(selector) {
196
293
  const target = formatSelector(selector);
197
294
  return this.addAction('check', target, async () => {
198
- await resolveSelector(this.getScope(), selector).check({ timeout: this.actionTimeout });
295
+ const locator = resolveSelector(this.getScope(), selector);
296
+ if (this.isKeyboard) {
297
+ await locator.waitFor({ state: 'visible', timeout: this.actionTimeout });
298
+ await tabToElement(this.page, locator, { timeout: this.actionTimeout });
299
+ await pressSpace(this.page);
300
+ }
301
+ else if (this.useFuzzyFingers) {
302
+ await fuzzyFingerCheck(this.page, locator, this.actionTimeout, selector);
303
+ }
304
+ else {
305
+ await locator.check({ timeout: this.actionTimeout });
306
+ }
199
307
  });
200
308
  }
201
309
  select(selector, value) {
202
310
  const target = `${formatSelector(selector)}=${value}`;
203
311
  return this.addAction('select', target, async () => {
204
- await resolveSelector(this.getScope(), selector).selectOption(value, { timeout: this.actionTimeout });
312
+ const locator = resolveSelector(this.getScope(), selector);
313
+ if (this.isKeyboard) {
314
+ await locator.waitFor({ state: 'visible', timeout: this.actionTimeout });
315
+ await tabToElement(this.page, locator, { timeout: this.actionTimeout });
316
+ await keyboardSelectOption(this.page, locator, value);
317
+ }
318
+ else if (this.useFuzzyFingers) {
319
+ await fuzzyFingerClick(this.page, locator, this.actionTimeout, selector);
320
+ await locator.selectOption(value, { timeout: this.actionTimeout });
321
+ }
322
+ else {
323
+ await locator.selectOption(value, { timeout: this.actionTimeout });
324
+ }
205
325
  });
206
326
  }
207
327
  wait(ms) {
@@ -219,6 +339,11 @@ class ActionChainImpl {
219
339
  await this.bus.waitFor(message);
220
340
  });
221
341
  }
342
+ pressKey(key) {
343
+ return this.addAction('pressKey', key, async () => {
344
+ await this.page.keyboard.press(key);
345
+ });
346
+ }
222
347
  do(fn) {
223
348
  return this.addAction('do', 'custom', async () => {
224
349
  await fn(this.page);
@@ -442,19 +567,23 @@ export class SequentialActorHandleImpl {
442
567
  actionTimeout;
443
568
  warnAfter;
444
569
  role;
445
- page;
446
- context;
570
+ _page;
571
+ _context;
447
572
  assertions = [];
573
+ navigationMode;
574
+ fuzzyFingers;
448
575
  // Registered watchers for conditional handling
449
576
  watchers = [];
450
577
  // Registered warning triggers
451
578
  warningTriggers = [];
579
+ // Page factory for switchDevice support
580
+ _pageFactory;
452
581
  // Forward config properties
453
582
  key;
454
583
  username;
455
584
  email;
456
585
  password;
457
- constructor(role, config, page, context, bus, timeline, warnings, actionTimeout, warnAfter) {
586
+ constructor(role, config, page, context, bus, timeline, warnings, actionTimeout, warnAfter, navigationMode = 'pointer', fuzzyFingers = false, pageFactory) {
458
587
  this.config = config;
459
588
  this.bus = bus;
460
589
  this.timeline = timeline;
@@ -462,9 +591,12 @@ export class SequentialActorHandleImpl {
462
591
  this.actionTimeout = actionTimeout;
463
592
  this.warnAfter = warnAfter;
464
593
  this.role = role;
465
- this.page = page;
466
- this.context = context;
594
+ this._page = page;
595
+ this._context = context;
596
+ this._pageFactory = pageFactory ?? null;
467
597
  this.key = config.key;
598
+ this.navigationMode = navigationMode;
599
+ this.fuzzyFingers = fuzzyFingers;
468
600
  // Copy all config properties to this instance
469
601
  for (const [k, value] of Object.entries(config)) {
470
602
  if (!(k in this)) {
@@ -475,6 +607,23 @@ export class SequentialActorHandleImpl {
475
607
  }
476
608
  }
477
609
  }
610
+ /** Playwright page for this actor */
611
+ get page() {
612
+ return this._page;
613
+ }
614
+ /** Playwright browser context for this actor */
615
+ get context() {
616
+ return this._context;
617
+ }
618
+ /** Get the page factory (used by ActionChainImpl for switchDevice) */
619
+ getPageFactory() {
620
+ return this._pageFactory;
621
+ }
622
+ /** Update page and context after a device switch */
623
+ _switchPage(page, context) {
624
+ this._page = page;
625
+ this._context = context;
626
+ }
478
627
  /**
479
628
  * Get registered watchers (used by ActionChainImpl)
480
629
  */
@@ -495,11 +644,23 @@ export class SequentialActorHandleImpl {
495
644
  this.watchers = [];
496
645
  }
497
646
  createChain() {
498
- return new ActionChainImpl(this, this.page, this.bus, this.timeline, this.warnings, this.actionTimeout, this.warnAfter);
647
+ return new ActionChainImpl(this, this.page, this.bus, this.timeline, this.warnings, this.actionTimeout, this.warnAfter, this.navigationMode, this.fuzzyFingers);
499
648
  }
500
649
  openTo(url) {
501
650
  return this.createChain().openTo(url);
502
651
  }
652
+ reload() {
653
+ return this.createChain().reload();
654
+ }
655
+ goBack() {
656
+ return this.createChain().goBack();
657
+ }
658
+ goForward() {
659
+ return this.createChain().goForward();
660
+ }
661
+ switchDevice(device) {
662
+ return this.createChain().switchDevice(device);
663
+ }
503
664
  see(selector) {
504
665
  return this.createChain().see(selector);
505
666
  }
@@ -536,6 +697,9 @@ export class SequentialActorHandleImpl {
536
697
  waitFor(message) {
537
698
  return this.createChain().waitFor(message);
538
699
  }
700
+ pressKey(key) {
701
+ return this.createChain().pressKey(key);
702
+ }
539
703
  do(fn) {
540
704
  return this.createChain().do(fn);
541
705
  }