@goliapkg/smix 1.0.27 → 2.1.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 (74) hide show
  1. package/README.md +53 -130
  2. package/dist/A11yNode.d.ts.map +1 -1
  3. package/dist/A11yNode.js +2 -3
  4. package/dist/A11yNode.js.map +1 -1
  5. package/dist/App.d.ts +37 -22
  6. package/dist/App.d.ts.map +1 -1
  7. package/dist/App.js +72 -66
  8. package/dist/App.js.map +1 -1
  9. package/dist/ExpectationFailure.d.ts +8 -1
  10. package/dist/ExpectationFailure.d.ts.map +1 -1
  11. package/dist/ExpectationFailure.js +7 -7
  12. package/dist/ExpectationFailure.js.map +1 -1
  13. package/dist/HttpRunner.d.ts +9 -31
  14. package/dist/HttpRunner.d.ts.map +1 -1
  15. package/dist/HttpRunner.js +14 -100
  16. package/dist/HttpRunner.js.map +1 -1
  17. package/dist/Locator.d.ts +7 -9
  18. package/dist/Locator.d.ts.map +1 -1
  19. package/dist/Locator.js +17 -18
  20. package/dist/Locator.js.map +1 -1
  21. package/dist/NodeDriver.d.ts +53 -0
  22. package/dist/NodeDriver.d.ts.map +1 -0
  23. package/dist/NodeDriver.js +70 -0
  24. package/dist/NodeDriver.js.map +1 -0
  25. package/dist/Pattern.js +2 -2
  26. package/dist/Pattern.js.map +1 -1
  27. package/dist/Selector.d.ts.map +1 -1
  28. package/dist/Selector.js +5 -8
  29. package/dist/Selector.js.map +1 -1
  30. package/dist/SelectorResolver.d.ts +4 -6
  31. package/dist/SelectorResolver.d.ts.map +1 -1
  32. package/dist/SelectorResolver.js +5 -8
  33. package/dist/SelectorResolver.js.map +1 -1
  34. package/dist/Smix.d.ts +12 -4
  35. package/dist/Smix.d.ts.map +1 -1
  36. package/dist/Smix.js +25 -23
  37. package/dist/Smix.js.map +1 -1
  38. package/dist/SystemPopup.d.ts +18 -0
  39. package/dist/SystemPopup.d.ts.map +1 -0
  40. package/dist/SystemPopup.js +7 -0
  41. package/dist/SystemPopup.js.map +1 -0
  42. package/dist/index.d.ts +3 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +2 -1
  45. package/dist/index.js.map +1 -1
  46. package/dist/loadNodeDriver.d.ts +21 -0
  47. package/dist/loadNodeDriver.d.ts.map +1 -0
  48. package/dist/loadNodeDriver.js +33 -0
  49. package/dist/loadNodeDriver.js.map +1 -0
  50. package/package.json +5 -2
  51. package/src/A11yNode.ts +2 -3
  52. package/src/App.ts +75 -78
  53. package/src/ExpectationFailure.ts +23 -13
  54. package/src/HttpRunner.ts +15 -126
  55. package/src/Locator.ts +17 -18
  56. package/src/NodeDriver.ts +109 -0
  57. package/src/Pattern.ts +2 -2
  58. package/src/Selector.ts +5 -8
  59. package/src/SelectorResolver.ts +7 -11
  60. package/src/Smix.ts +30 -26
  61. package/src/SystemPopup.ts +24 -0
  62. package/src/__tests__/AppDriving.test.ts +106 -0
  63. package/src/__tests__/MvpApiShape.test.ts +78 -46
  64. package/src/__tests__/ReadmeSnippets.test.ts +132 -0
  65. package/src/__tests__/RealAddonLoopback.test.ts +81 -0
  66. package/src/__tests__/SelectorFullSchema.test.ts +2 -4
  67. package/src/index.ts +19 -8
  68. package/src/loadNodeDriver.ts +41 -0
  69. package/src/SimRuntime.ts +0 -145
  70. package/src/__tests__/AppFullApi.test.ts +0 -168
  71. package/src/__tests__/AppTapMock.test.ts +0 -122
  72. package/src/__tests__/HttpRunner.test.ts +0 -121
  73. package/src/__tests__/Locator.test.ts +0 -128
  74. package/src/__tests__/LocatorToHave.test.ts +0 -118
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import http from 'node:http'
3
+ import { App } from '../App.js'
4
+ import { loadNodeDriver } from '../loadNodeDriver.js'
5
+ import { MockSelectorResolver } from '../SelectorResolver.js'
6
+ import { Selector, encodeSelectorJson } from '../Selector.js'
7
+ import { Smix, bundleId } from '../Smix.js'
8
+
9
+ // Drives through the REAL @goliapkg/smix-node addon (resolved via the bun
10
+ // workspace) against a node:http loopback fake wire — no device, no publish.
11
+ // This is the seam's production leg: a real SmixNodeDriver satisfies NodeDriver
12
+ // and crosses napi to the loopback, the same shape C2 loopback-tested at the
13
+ // crate, now reached through smix-rn's loadNodeDriver factory.
14
+ const A11Y_ROOT =
15
+ '{"rawType":"application","identifier":"root","enabled":true,"selected":false,"hasFocus":false,"visible":true,"bounds":{"x":0,"y":0,"w":0,"h":0}}'
16
+
17
+ interface Wire {
18
+ port: number
19
+ seen: Record<string, unknown>
20
+ close: () => Promise<void>
21
+ }
22
+
23
+ async function startFakeWire(): Promise<Wire> {
24
+ const seen: Record<string, unknown> = {}
25
+ const server = http.createServer((req, res) => {
26
+ let body = ''
27
+ req.on('data', (c) => (body += c))
28
+ req.on('end', () => {
29
+ const parsed = body ? JSON.parse(body) : {}
30
+ seen[req.url ?? ''] = parsed
31
+ const reply =
32
+ req.url === '/tree'
33
+ ? A11Y_ROOT
34
+ : req.url === '/session/open'
35
+ ? '{"sessionId":"s-1"}'
36
+ : '{"ok":true}'
37
+ res.writeHead(200, { 'content-type': 'application/json' })
38
+ res.end(reply)
39
+ })
40
+ })
41
+ await new Promise<void>((r) => server.listen(0, '127.0.0.1', () => r()))
42
+ const addr = server.address()
43
+ const port = typeof addr === 'object' && addr ? addr.port : 0
44
+ return { port, seen, close: () => new Promise((r) => server.close(() => r())) }
45
+ }
46
+
47
+ describe('smix-rn drives through the real napi addon (C4)', () => {
48
+ it('loadNodeDriver resolves a real SmixNodeDriver satisfying the seam', async () => {
49
+ const driver = await loadNodeDriver(1)
50
+ expect(typeof driver.snapshotTree).toBe('function')
51
+ expect(typeof driver.tapById).toBe('function')
52
+ expect(typeof driver.openSession).toBe('function')
53
+ })
54
+
55
+ it('snapshotTree crosses the real .node to the loopback wire', async () => {
56
+ const wire = await startFakeWire()
57
+ try {
58
+ const driver = await loadNodeDriver(wire.port)
59
+ const tree = JSON.parse(await driver.snapshotTree())
60
+ expect(tree.identifier).toBe('root')
61
+ } finally {
62
+ await wire.close()
63
+ }
64
+ })
65
+
66
+ it('Smix.launchApp with an explicit real driver launches and taps', async () => {
67
+ const wire = await startFakeWire()
68
+ try {
69
+ const driver = await loadNodeDriver(wire.port)
70
+ const resolver = new MockSelectorResolver()
71
+ resolver.registerHit(encodeSelectorJson(Selector.id('root')), 'root')
72
+ const app = await Smix.launchApp(bundleId('com.acme.app'), { driver, resolver: resolver.resolve })
73
+ expect(app).toBeInstanceOf(App)
74
+
75
+ await app.tap(Selector.id('root'))
76
+ expect((wire.seen['/tap-by-id'] as { id?: string })?.id).toBe('root')
77
+ } finally {
78
+ await wire.close()
79
+ }
80
+ })
81
+ })
@@ -1,7 +1,5 @@
1
- // v7.5 c1 — Selector full 7-case + Modifiers flatten + Pattern + fluent
2
- // chaining roundtrip + Rust-compatible wire shape tests.
3
- //
4
- // Mirror Kotlin SelectorFullSchemaTest + Swift SelectorFullSchemaTests.
1
+ // Selector's seven cases, Modifiers flattening, Pattern, fluent chaining
2
+ // roundtrip, and the wire shape the Rust core expects.
5
3
 
6
4
  import { describe, expect, test } from 'vitest'
7
5
  import {
package/src/index.ts CHANGED
@@ -8,6 +8,25 @@ export {
8
8
  App,
9
9
  } from './App.js'
10
10
 
11
+ export {
12
+ MockNodeDriver,
13
+ MockNodeSession,
14
+ type NodeDriver,
15
+ type NodeSession,
16
+ type RecordedCall,
17
+ } from './NodeDriver.js'
18
+
19
+ export {
20
+ type SystemPopup,
21
+ type SystemPopupButton,
22
+ } from './SystemPopup.js'
23
+
24
+ export {
25
+ defaultRunnerPort,
26
+ loadNodeDriver,
27
+ loadNodeResolver,
28
+ } from './loadNodeDriver.js'
29
+
11
30
  export {
12
31
  literal,
13
32
  patternFromJson,
@@ -46,14 +65,6 @@ export {
46
65
  type AppTarget,
47
66
  } from './Smix.js'
48
67
 
49
- export {
50
- MockSimRuntime,
51
- type KeyName,
52
- type LaunchFreshCall,
53
- type SmixSimRuntime,
54
- type SwipeDirection,
55
- } from './SimRuntime.js'
56
-
57
68
  export {
58
69
  MockLabelsResolver,
59
70
  MockSelectorResolver,
@@ -0,0 +1,41 @@
1
+ // The production leg of the NodeDriver seam: load the real napi addon
2
+ // (`@goliapkg/smix-node`) and construct a driver aimed at a runner port. The
3
+ // import is dynamic and lazy so a platform without a prebuilt `.node` does not
4
+ // fail at install/import time — it fails clearly here, only when a caller
5
+ // actually reaches for live driving. The addon is an optionalDependency, so
6
+ // this is the single place its absence surfaces.
7
+
8
+ import type { NodeDriver } from './NodeDriver.js'
9
+ import type { LabelsResolver, SelectorResolver } from './SelectorResolver.js'
10
+
11
+ /** The runner port to drive, from `SMIX_RUNNER_PORT` or the CLI default. */
12
+ export function defaultRunnerPort(): number {
13
+ return Number(process.env.SMIX_RUNNER_PORT) || 22087
14
+ }
15
+
16
+ /**
17
+ * Load the real `SmixNodeDriver` and aim it at `port` (defaulting to the
18
+ * runner port). The returned instance satisfies `NodeDriver` structurally.
19
+ */
20
+ export async function loadNodeDriver(port?: number): Promise<NodeDriver> {
21
+ const { SmixNodeDriver } = await import('@goliapkg/smix-node')
22
+ return new SmixNodeDriver(port ?? defaultRunnerPort()) as unknown as NodeDriver
23
+ }
24
+
25
+ /**
26
+ * Load a selector resolver backed by the napi addon's stone resolver. This
27
+ * resolves host-side — in this process, over napi — the same as the Swift /
28
+ * Kotlin SDKs resolve through UniFFI, rather than posting to a runner
29
+ * `/select/resolve` route the iOS runner does not serve. No port: resolution
30
+ * is a pure function of the tree and selector.
31
+ */
32
+ export async function loadNodeResolver(): Promise<{
33
+ resolver: SelectorResolver
34
+ labelsResolver: LabelsResolver
35
+ }> {
36
+ const { resolveSelector, resolveSelectorLabels } = await import('@goliapkg/smix-node')
37
+ return {
38
+ resolver: async (treeJson, selectorJson) => resolveSelector(treeJson, selectorJson),
39
+ labelsResolver: async (treeJson, selectorJson) => resolveSelectorLabels(treeJson, selectorJson),
40
+ }
41
+ }
package/src/SimRuntime.ts DELETED
@@ -1,145 +0,0 @@
1
- // SmixSimRuntime interface + MockSimRuntime impl.
2
- //
3
- // Mirrors the Swift and Kotlin SimRuntime architecture. The TS SDK is
4
- // HTTP-runner-friendly:
5
- // - HttpSimRuntime (real, lives in src/HttpRunner.ts): wraps the
6
- // smix-runner HTTP server via fetch (cross-platform iOS+Android).
7
- // - MockSimRuntime (here): in-memory tree + event log for unit tests.
8
- //
9
- // This module is intended for test targets only.
10
-
11
- import type { A11yNode } from './A11yNode.js'
12
-
13
- export type KeyName = 'return' | 'delete' | 'space' | 'tab' | 'escape' | 'enter'
14
- export type SwipeDirection = 'up' | 'down' | 'left' | 'right'
15
-
16
- /** Low-level emulator runtime — captures + injects events. */
17
- export interface SmixSimRuntime {
18
- launch(bundleId: string): Promise<void>
19
- terminate(bundleId: string): Promise<void>
20
- snapshotTree(): Promise<A11yNode>
21
- synthesizeTap(x: number, y: number): Promise<void>
22
- sendString(text: string): Promise<void>
23
- pressKey(key: KeyName): Promise<void>
24
- swipe(direction: SwipeDirection): Promise<void>
25
- screenshot(): Promise<Uint8Array>
26
- systemPopups(): Promise<A11yNode[]>
27
- openUrl(url: string): Promise<void>
28
- launchFresh(opts: {
29
- bundleId: string
30
- clearState: boolean
31
- clearKeychain: boolean
32
- appPath?: string | undefined
33
- }): Promise<void>
34
- launchFromPath(appPath: string): Promise<void>
35
- synthesizeTapAtNormalized(nx: number, ny: number): Promise<void>
36
- }
37
-
38
- export interface LaunchFreshCall {
39
- bundleId: string
40
- clearState: boolean
41
- clearKeychain: boolean
42
- appPath: string | undefined
43
- }
44
-
45
- const EMPTY_DEFAULT: A11yNode = {
46
- rawType: 'other',
47
- bounds: { x: 0, y: 0, w: 393, h: 852 },
48
- enabled: true,
49
- visible: true,
50
- children: [],
51
- }
52
-
53
- /**
54
- * In-memory mock runtime — used by vitest unit tests for deterministic
55
- * wire verification without booting an emulator.
56
- */
57
- export class MockSimRuntime implements SmixSimRuntime {
58
- snapshotResult: A11yNode = EMPTY_DEFAULT
59
- readonly tapCalls: { x: number; y: number }[] = []
60
- readonly launchCalls: string[] = []
61
- readonly terminateCalls: string[] = []
62
- readonly sendStringCalls: string[] = []
63
- readonly pressKeyCalls: KeyName[] = []
64
- readonly swipeCalls: SwipeDirection[] = []
65
- screenshotCalls = 0
66
- screenshotResult: Uint8Array = new Uint8Array(0)
67
- systemPopupsResult: A11yNode[] = []
68
- readonly openUrlCalls: string[] = []
69
- readonly launchFreshCalls: LaunchFreshCall[] = []
70
- readonly launchFromPathCalls: string[] = []
71
- readonly tapAtNormalizedCalls: { nx: number; ny: number }[] = []
72
-
73
- /** Hook invoked just before [snapshotTree] returns. Mirrors Swift/Kotlin. */
74
- afterSnapshot: ((callIndex: number) => void) | null = null
75
- private snapshotCallCount = 0
76
-
77
- constructor(opts: { snapshotResult?: A11yNode } = {}) {
78
- if (opts.snapshotResult !== undefined) this.snapshotResult = opts.snapshotResult
79
- }
80
-
81
- async launch(bundleId: string): Promise<void> {
82
- this.launchCalls.push(bundleId)
83
- }
84
-
85
- async terminate(bundleId: string): Promise<void> {
86
- this.terminateCalls.push(bundleId)
87
- }
88
-
89
- async snapshotTree(): Promise<A11yNode> {
90
- const idx = this.snapshotCallCount++
91
- this.afterSnapshot?.(idx)
92
- return this.snapshotResult
93
- }
94
-
95
- async synthesizeTap(x: number, y: number): Promise<void> {
96
- this.tapCalls.push({ x, y })
97
- }
98
-
99
- async sendString(text: string): Promise<void> {
100
- this.sendStringCalls.push(text)
101
- }
102
-
103
- async pressKey(key: KeyName): Promise<void> {
104
- this.pressKeyCalls.push(key)
105
- }
106
-
107
- async swipe(direction: SwipeDirection): Promise<void> {
108
- this.swipeCalls.push(direction)
109
- }
110
-
111
- async screenshot(): Promise<Uint8Array> {
112
- this.screenshotCalls++
113
- return this.screenshotResult
114
- }
115
-
116
- async systemPopups(): Promise<A11yNode[]> {
117
- return this.systemPopupsResult
118
- }
119
-
120
- async openUrl(url: string): Promise<void> {
121
- this.openUrlCalls.push(url)
122
- }
123
-
124
- async launchFresh(opts: {
125
- bundleId: string
126
- clearState: boolean
127
- clearKeychain: boolean
128
- appPath?: string | undefined
129
- }): Promise<void> {
130
- this.launchFreshCalls.push({
131
- bundleId: opts.bundleId,
132
- clearState: opts.clearState,
133
- clearKeychain: opts.clearKeychain,
134
- appPath: opts.appPath,
135
- })
136
- }
137
-
138
- async launchFromPath(appPath: string): Promise<void> {
139
- this.launchFromPathCalls.push(appPath)
140
- }
141
-
142
- async synthesizeTapAtNormalized(nx: number, ny: number): Promise<void> {
143
- this.tapAtNormalizedCalls.push({ nx, ny })
144
- }
145
- }
@@ -1,168 +0,0 @@
1
- // v7.5 c3 — App act/sense full API wire tests, mirror Kotlin
2
- // AppActSenseExtMockTest + AppFillPressKeyMockTest + Swift v7.2 c1/c2/c5.
3
-
4
- import { describe, expect, test } from 'vitest'
5
- import {
6
- bundleId,
7
- ExpectationFailure,
8
- literal,
9
- MockSelectorResolver,
10
- MockSimRuntime,
11
- Selector,
12
- Smix,
13
- type A11yNode,
14
- } from '../index.js'
15
-
16
- describe('App.fill / pressKey', () => {
17
- test('fill = tap-to-focus + sendString', async () => {
18
- const field: A11yNode = {
19
- rawType: 'textField',
20
- identifier: 'input-username',
21
- label: 'Username',
22
- bounds: { x: 50, y: 200, w: 200, h: 40 },
23
- enabled: true,
24
- visible: true,
25
- }
26
- const tree: A11yNode = {
27
- rawType: 'other',
28
- bounds: { x: 0, y: 0, w: 393, h: 852 },
29
- visible: true,
30
- children: [field],
31
- }
32
- const runtime = new MockSimRuntime({ snapshotResult: tree })
33
- const resolver = new MockSelectorResolver()
34
- resolver.registerHit('{"id":"input-username"}', 'input-username')
35
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, resolver.resolve)
36
-
37
- await app.fill(Selector.id('input-username'), 'alice')
38
-
39
- expect(runtime.tapCalls.length).toBe(1)
40
- expect(runtime.tapCalls[0]?.x).toBeCloseTo(150, 1)
41
- expect(runtime.tapCalls[0]?.y).toBeCloseTo(220, 1)
42
- expect(runtime.sendStringCalls).toEqual(['alice'])
43
- })
44
-
45
- test('pressKey delegates to runtime', async () => {
46
- const runtime = new MockSimRuntime()
47
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
48
- await app.pressKey('return')
49
- await app.pressKey('delete')
50
- expect(runtime.pressKeyCalls).toEqual(['return', 'delete'])
51
- })
52
-
53
- test('fill failure surfaces ExpectationFailure (no sendString fired)', async () => {
54
- const runtime = new MockSimRuntime()
55
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
56
- await expect(app.fill(Selector.id('missing'), 'text')).rejects.toBeInstanceOf(ExpectationFailure)
57
- expect(runtime.sendStringCalls).toEqual([])
58
- })
59
- })
60
-
61
- describe('App.swipe / screenshot / systemPopups / openUrl', () => {
62
- test('swipe delegates with 4 directions', async () => {
63
- const runtime = new MockSimRuntime()
64
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
65
- await app.swipe('down')
66
- await app.swipe('up')
67
- await app.swipe('left')
68
- await app.swipe('right')
69
- expect(runtime.swipeCalls).toEqual(['down', 'up', 'left', 'right'])
70
- })
71
-
72
- test('screenshot returns runtime bytes', async () => {
73
- const expected = new Uint8Array([0x89, 0x50, 0x4e, 0x47])
74
- const runtime = new MockSimRuntime()
75
- runtime.screenshotResult = expected
76
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
77
- const out = await app.screenshot()
78
- expect(out).toEqual(expected)
79
- expect(runtime.screenshotCalls).toBe(1)
80
- })
81
-
82
- test('systemPopups returns runtime list', async () => {
83
- const alert: A11yNode = {
84
- rawType: 'alert',
85
- identifier: 'alert-permission',
86
- bounds: { x: 0, y: 0, w: 200, h: 100 },
87
- }
88
- const runtime = new MockSimRuntime()
89
- runtime.systemPopupsResult = [alert]
90
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
91
- const popups = await app.systemPopups()
92
- expect(popups.length).toBe(1)
93
- expect(popups[0]?.identifier).toBe('alert-permission')
94
- })
95
-
96
- test('openUrl forwards to runtime', async () => {
97
- const runtime = new MockSimRuntime()
98
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
99
- await app.openUrl('smix://deep/link?id=42')
100
- expect(runtime.openUrlCalls).toEqual(['smix://deep/link?id=42'])
101
- })
102
- })
103
-
104
- describe('App.launchFresh', () => {
105
- test('clearState + clearKeychain', async () => {
106
- const runtime = new MockSimRuntime()
107
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
108
- await app.launchFresh({ clearState: true, clearKeychain: true })
109
- expect(runtime.launchFreshCalls.length).toBe(1)
110
- const call = runtime.launchFreshCalls[0]!
111
- expect(call.bundleId).toBe('dev.smix.fixture')
112
- expect(call.clearState).toBe(true)
113
- expect(call.clearKeychain).toBe(true)
114
- expect(call.appPath).toBeUndefined()
115
- })
116
-
117
- test('with appPath override', async () => {
118
- const runtime = new MockSimRuntime()
119
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
120
- await app.launchFresh({ appPath: '/tmp/Reinstall.apk' })
121
- expect(runtime.launchFreshCalls[0]?.appPath).toBe('/tmp/Reinstall.apk')
122
- })
123
-
124
- test('defaults are false', async () => {
125
- const runtime = new MockSimRuntime()
126
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
127
- await app.launchFresh()
128
- const call = runtime.launchFreshCalls[0]!
129
- expect(call.clearState).toBe(false)
130
- expect(call.clearKeychain).toBe(false)
131
- })
132
- })
133
-
134
- describe('App.tapAtCoord', () => {
135
- test('valid range', async () => {
136
- const runtime = new MockSimRuntime()
137
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
138
- await app.tapAtCoord(0.5, 0.5)
139
- expect(runtime.tapAtNormalizedCalls.length).toBe(1)
140
- expect(runtime.tapAtNormalizedCalls[0]).toEqual({ nx: 0.5, ny: 0.5 })
141
- })
142
-
143
- test('edge values 0,0 and 1,1', async () => {
144
- const runtime = new MockSimRuntime()
145
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
146
- await app.tapAtCoord(0, 0)
147
- await app.tapAtCoord(1, 1)
148
- expect(runtime.tapAtNormalizedCalls.length).toBe(2)
149
- })
150
-
151
- test('out of range nx throws wrongState', async () => {
152
- const runtime = new MockSimRuntime()
153
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
154
- await expect(app.tapAtCoord(1.5, 0.5)).rejects.toMatchObject({
155
- name: 'ExpectationFailure',
156
- code: 'wrongState',
157
- })
158
- expect(runtime.tapAtNormalizedCalls).toEqual([])
159
- })
160
-
161
- test('negative ny throws wrongState', async () => {
162
- const runtime = new MockSimRuntime()
163
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, new MockSelectorResolver().resolve)
164
- await expect(app.tapAtCoord(0.5, -0.1)).rejects.toMatchObject({
165
- code: 'wrongState',
166
- })
167
- })
168
- })
@@ -1,122 +0,0 @@
1
- // v7.5 c1 — App.tap mock-based unit tests.
2
- //
3
- // Mirror Kotlin AppTapMockTest + Swift AppTapMockTests. Verifies wire
4
- // pipeline (snapshot → SelectorResolver → tap synthesize) end-to-end
5
- // against MockSimRuntime + MockSelectorResolver.
6
-
7
- import { describe, expect, test } from 'vitest'
8
- import {
9
- bundleId,
10
- ExpectationFailure,
11
- literal,
12
- MockSelectorResolver,
13
- MockSimRuntime,
14
- Selector,
15
- Smix,
16
- type A11yNode,
17
- } from '../index.js'
18
-
19
- describe('App.tap notFound path', () => {
20
- test('empty resolver result yields notFound', async () => {
21
- const runtime = new MockSimRuntime()
22
- const resolver = new MockSelectorResolver()
23
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, resolver.resolve)
24
- await expect(app.tap(Selector.id('btn-missing'))).rejects.toMatchObject({
25
- name: 'ExpectationFailure',
26
- code: 'notFound',
27
- })
28
- })
29
- })
30
-
31
- describe('App.tap hit path', () => {
32
- test('id hit synthesizes at bounds center', async () => {
33
- const button: A11yNode = {
34
- rawType: 'button',
35
- identifier: 'btn-login',
36
- label: 'Sign In',
37
- bounds: { x: 100, y: 200, w: 80, h: 40 },
38
- enabled: true,
39
- visible: true,
40
- }
41
- const root: A11yNode = {
42
- rawType: 'other',
43
- bounds: { x: 0, y: 0, w: 393, h: 852 },
44
- enabled: true,
45
- visible: true,
46
- children: [button],
47
- }
48
- const runtime = new MockSimRuntime({ snapshotResult: root })
49
- const resolver = new MockSelectorResolver()
50
- resolver.registerHit('{"id":"btn-login"}', 'btn-login')
51
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, resolver.resolve)
52
-
53
- await app.tap(Selector.id('btn-login'))
54
-
55
- expect(runtime.tapCalls.length).toBe(1)
56
- expect(runtime.tapCalls[0]?.x).toBeCloseTo(140, 1)
57
- expect(runtime.tapCalls[0]?.y).toBeCloseTo(220, 1)
58
- })
59
-
60
- test('label hit synthesizes at center', async () => {
61
- const button: A11yNode = {
62
- rawType: 'button',
63
- identifier: 'btn-foo',
64
- label: 'Submit',
65
- bounds: { x: 50, y: 600, w: 100, h: 50 },
66
- visible: true,
67
- }
68
- const root: A11yNode = {
69
- rawType: 'other',
70
- bounds: { x: 0, y: 0, w: 393, h: 852 },
71
- visible: true,
72
- children: [button],
73
- }
74
- const runtime = new MockSimRuntime({ snapshotResult: root })
75
- const resolver = new MockSelectorResolver()
76
- resolver.registerHit('{"label":"Submit"}', 'btn-foo')
77
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, resolver.resolve)
78
-
79
- await app.tap(Selector.label('Submit'))
80
- expect(runtime.tapCalls.length).toBe(1)
81
- expect(runtime.tapCalls[0]?.x).toBeCloseTo(100, 1)
82
- })
83
- })
84
-
85
- describe('App.tap passes correct selectorJson', () => {
86
- test('Rust-compatible untagged shape', async () => {
87
- const root: A11yNode = {
88
- rawType: 'other',
89
- bounds: { x: 0, y: 0, w: 393, h: 852 },
90
- visible: true,
91
- children: [],
92
- }
93
- const runtime = new MockSimRuntime({ snapshotResult: root })
94
- const resolver = new MockSelectorResolver()
95
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, resolver.resolve)
96
-
97
- await expect(app.tap(Selector.id('btn-x'))).rejects.toBeInstanceOf(ExpectationFailure)
98
-
99
- expect(resolver.calls.length).toBe(1)
100
- expect(resolver.calls[0]?.selectorJson).toBe('{"id":"btn-x"}')
101
- })
102
- })
103
-
104
- describe('App.tap resolver error surfaces as ExpectationFailure', () => {
105
- test('throws unknown when resolver raises', async () => {
106
- const root: A11yNode = {
107
- rawType: 'other',
108
- bounds: { x: 0, y: 0, w: 393, h: 852 },
109
- visible: true,
110
- children: [],
111
- }
112
- const runtime = new MockSimRuntime({ snapshotResult: root })
113
- const resolver = new MockSelectorResolver()
114
- resolver.throwOnNext = new Error('simulated FFI parse error')
115
- const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, resolver.resolve)
116
-
117
- await expect(app.tap(Selector.id('anything'))).rejects.toMatchObject({
118
- name: 'ExpectationFailure',
119
- code: 'unknown',
120
- })
121
- })
122
- })