@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.
- package/README.md +53 -130
- package/dist/A11yNode.d.ts.map +1 -1
- package/dist/A11yNode.js +2 -3
- package/dist/A11yNode.js.map +1 -1
- package/dist/App.d.ts +37 -22
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +72 -66
- package/dist/App.js.map +1 -1
- package/dist/ExpectationFailure.d.ts +8 -1
- package/dist/ExpectationFailure.d.ts.map +1 -1
- package/dist/ExpectationFailure.js +7 -7
- package/dist/ExpectationFailure.js.map +1 -1
- package/dist/HttpRunner.d.ts +9 -31
- package/dist/HttpRunner.d.ts.map +1 -1
- package/dist/HttpRunner.js +14 -100
- package/dist/HttpRunner.js.map +1 -1
- package/dist/Locator.d.ts +7 -9
- package/dist/Locator.d.ts.map +1 -1
- package/dist/Locator.js +17 -18
- package/dist/Locator.js.map +1 -1
- package/dist/NodeDriver.d.ts +53 -0
- package/dist/NodeDriver.d.ts.map +1 -0
- package/dist/NodeDriver.js +70 -0
- package/dist/NodeDriver.js.map +1 -0
- package/dist/Pattern.js +2 -2
- package/dist/Pattern.js.map +1 -1
- package/dist/Selector.d.ts.map +1 -1
- package/dist/Selector.js +5 -8
- package/dist/Selector.js.map +1 -1
- package/dist/SelectorResolver.d.ts +4 -6
- package/dist/SelectorResolver.d.ts.map +1 -1
- package/dist/SelectorResolver.js +5 -8
- package/dist/SelectorResolver.js.map +1 -1
- package/dist/Smix.d.ts +12 -4
- package/dist/Smix.d.ts.map +1 -1
- package/dist/Smix.js +25 -23
- package/dist/Smix.js.map +1 -1
- package/dist/SystemPopup.d.ts +18 -0
- package/dist/SystemPopup.d.ts.map +1 -0
- package/dist/SystemPopup.js +7 -0
- package/dist/SystemPopup.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/loadNodeDriver.d.ts +21 -0
- package/dist/loadNodeDriver.d.ts.map +1 -0
- package/dist/loadNodeDriver.js +33 -0
- package/dist/loadNodeDriver.js.map +1 -0
- package/package.json +5 -2
- package/src/A11yNode.ts +2 -3
- package/src/App.ts +75 -78
- package/src/ExpectationFailure.ts +23 -13
- package/src/HttpRunner.ts +15 -126
- package/src/Locator.ts +17 -18
- package/src/NodeDriver.ts +109 -0
- package/src/Pattern.ts +2 -2
- package/src/Selector.ts +5 -8
- package/src/SelectorResolver.ts +7 -11
- package/src/Smix.ts +30 -26
- package/src/SystemPopup.ts +24 -0
- package/src/__tests__/AppDriving.test.ts +106 -0
- package/src/__tests__/MvpApiShape.test.ts +78 -46
- package/src/__tests__/ReadmeSnippets.test.ts +132 -0
- package/src/__tests__/RealAddonLoopback.test.ts +81 -0
- package/src/__tests__/SelectorFullSchema.test.ts +2 -4
- package/src/index.ts +19 -8
- package/src/loadNodeDriver.ts +41 -0
- package/src/SimRuntime.ts +0 -145
- package/src/__tests__/AppFullApi.test.ts +0 -168
- package/src/__tests__/AppTapMock.test.ts +0 -122
- package/src/__tests__/HttpRunner.test.ts +0 -121
- package/src/__tests__/Locator.test.ts +0 -128
- 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
|
-
//
|
|
2
|
-
//
|
|
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
|
-
})
|