@goliapkg/smix 0.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 +193 -0
- package/dist/A11yNode.d.ts +36 -0
- package/dist/A11yNode.d.ts.map +1 -0
- package/dist/A11yNode.js +30 -0
- package/dist/A11yNode.js.map +1 -0
- package/dist/App.d.ts +57 -0
- package/dist/App.d.ts.map +1 -0
- package/dist/App.js +123 -0
- package/dist/App.js.map +1 -0
- package/dist/ExpectationFailure.d.ts +30 -0
- package/dist/ExpectationFailure.d.ts.map +1 -0
- package/dist/ExpectationFailure.js +47 -0
- package/dist/ExpectationFailure.js.map +1 -0
- package/dist/HttpRunner.d.ts +61 -0
- package/dist/HttpRunner.d.ts.map +1 -0
- package/dist/HttpRunner.js +148 -0
- package/dist/HttpRunner.js.map +1 -0
- package/dist/Locator.d.ts +60 -0
- package/dist/Locator.d.ts.map +1 -0
- package/dist/Locator.js +177 -0
- package/dist/Locator.js.map +1 -0
- package/dist/Pattern.d.ts +21 -0
- package/dist/Pattern.d.ts.map +1 -0
- package/dist/Pattern.js +44 -0
- package/dist/Pattern.js.map +1 -0
- package/dist/Selector.d.ts +96 -0
- package/dist/Selector.d.ts.map +1 -0
- package/dist/Selector.js +276 -0
- package/dist/Selector.js.map +1 -0
- package/dist/SelectorResolver.d.ts +41 -0
- package/dist/SelectorResolver.d.ts.map +1 -0
- package/dist/SelectorResolver.js +55 -0
- package/dist/SelectorResolver.js.map +1 -0
- package/dist/SimRuntime.d.ts +81 -0
- package/dist/SimRuntime.d.ts.map +1 -0
- package/dist/SimRuntime.js +91 -0
- package/dist/SimRuntime.js.map +1 -0
- package/dist/Smix.d.ts +22 -0
- package/dist/Smix.d.ts.map +1 -0
- package/dist/Smix.js +35 -0
- package/dist/Smix.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
- package/src/A11yNode.ts +62 -0
- package/src/App.ts +153 -0
- package/src/ExpectationFailure.ts +67 -0
- package/src/HttpRunner.ts +185 -0
- package/src/Locator.ts +190 -0
- package/src/Pattern.ts +51 -0
- package/src/Selector.ts +296 -0
- package/src/SelectorResolver.ts +76 -0
- package/src/SimRuntime.ts +145 -0
- package/src/Smix.ts +49 -0
- package/src/__tests__/AppFullApi.test.ts +168 -0
- package/src/__tests__/AppTapMock.test.ts +122 -0
- package/src/__tests__/HttpRunner.test.ts +121 -0
- package/src/__tests__/Locator.test.ts +128 -0
- package/src/__tests__/LocatorToHave.test.ts +118 -0
- package/src/__tests__/MvpApiShape.test.ts +117 -0
- package/src/__tests__/SelectorFullSchema.test.ts +161 -0
- package/src/index.ts +76 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// v7.5 c3 — Locator poll loop tests, mirror Kotlin LocatorMockTest +
|
|
2
|
+
// Swift LocatorMockTests.
|
|
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('Locator.toBeVisible', () => {
|
|
17
|
+
test('succeeds when initially visible', async () => {
|
|
18
|
+
const node: A11yNode = {
|
|
19
|
+
rawType: 'button',
|
|
20
|
+
identifier: 'btn-x',
|
|
21
|
+
label: 'x',
|
|
22
|
+
bounds: { x: 0, y: 0, w: 10, h: 10 },
|
|
23
|
+
visible: true,
|
|
24
|
+
}
|
|
25
|
+
const tree: A11yNode = {
|
|
26
|
+
rawType: 'other',
|
|
27
|
+
bounds: { x: 0, y: 0, w: 100, h: 100 },
|
|
28
|
+
visible: true,
|
|
29
|
+
children: [node],
|
|
30
|
+
}
|
|
31
|
+
const runtime = new MockSimRuntime({ snapshotResult: tree })
|
|
32
|
+
const resolver = new MockSelectorResolver()
|
|
33
|
+
resolver.registerHit('{"id":"btn-x"}', 'btn-x')
|
|
34
|
+
const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, resolver.resolve)
|
|
35
|
+
await app.find(Selector.id('btn-x')).toBeVisible({ timeoutMs: 500 })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('timeout when never matches', async () => {
|
|
39
|
+
const tree: A11yNode = {
|
|
40
|
+
rawType: 'other',
|
|
41
|
+
bounds: { x: 0, y: 0, w: 100, h: 100 },
|
|
42
|
+
visible: true,
|
|
43
|
+
children: [],
|
|
44
|
+
}
|
|
45
|
+
const runtime = new MockSimRuntime({ snapshotResult: tree })
|
|
46
|
+
const resolver = new MockSelectorResolver()
|
|
47
|
+
const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, resolver.resolve)
|
|
48
|
+
await expect(
|
|
49
|
+
app.find(Selector.id('btn-missing')).toBeVisible({ timeoutMs: 300 }),
|
|
50
|
+
).rejects.toMatchObject({
|
|
51
|
+
name: 'ExpectationFailure',
|
|
52
|
+
code: 'timeout',
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('wrongState when matched but hidden', async () => {
|
|
57
|
+
const node: A11yNode = {
|
|
58
|
+
rawType: 'button',
|
|
59
|
+
identifier: 'btn-y',
|
|
60
|
+
label: 'y',
|
|
61
|
+
bounds: { x: 0, y: 0, w: 10, h: 10 },
|
|
62
|
+
visible: false,
|
|
63
|
+
}
|
|
64
|
+
const tree: A11yNode = {
|
|
65
|
+
rawType: 'other',
|
|
66
|
+
bounds: { x: 0, y: 0, w: 100, h: 100 },
|
|
67
|
+
visible: true,
|
|
68
|
+
children: [node],
|
|
69
|
+
}
|
|
70
|
+
const runtime = new MockSimRuntime({ snapshotResult: tree })
|
|
71
|
+
const resolver = new MockSelectorResolver()
|
|
72
|
+
resolver.registerHit('{"id":"btn-y"}', 'btn-y')
|
|
73
|
+
const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, resolver.resolve)
|
|
74
|
+
await expect(
|
|
75
|
+
app.find(Selector.id('btn-y')).toBeVisible({ timeoutMs: 200 }),
|
|
76
|
+
).rejects.toMatchObject({
|
|
77
|
+
name: 'ExpectationFailure',
|
|
78
|
+
code: 'wrongState',
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('Locator.toContainText', () => {
|
|
84
|
+
test('succeeds when label matches', async () => {
|
|
85
|
+
const node: A11yNode = {
|
|
86
|
+
rawType: 'staticText',
|
|
87
|
+
identifier: 'msg',
|
|
88
|
+
label: 'Welcome back, Alice!',
|
|
89
|
+
bounds: { x: 0, y: 0, w: 100, h: 24 },
|
|
90
|
+
visible: true,
|
|
91
|
+
}
|
|
92
|
+
const tree: A11yNode = {
|
|
93
|
+
rawType: 'other',
|
|
94
|
+
bounds: { x: 0, y: 0, w: 100, h: 100 },
|
|
95
|
+
children: [node],
|
|
96
|
+
}
|
|
97
|
+
const runtime = new MockSimRuntime({ snapshotResult: tree })
|
|
98
|
+
const resolver = new MockSelectorResolver()
|
|
99
|
+
resolver.registerHit('{"id":"msg"}', 'msg')
|
|
100
|
+
const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, resolver.resolve)
|
|
101
|
+
await app.find(Selector.id('msg')).toContainText('Alice', { timeoutMs: 500 })
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('timeout when never matches', async () => {
|
|
105
|
+
const node: A11yNode = {
|
|
106
|
+
rawType: 'staticText',
|
|
107
|
+
identifier: 'msg',
|
|
108
|
+
label: 'Hello, World',
|
|
109
|
+
bounds: { x: 0, y: 0, w: 100, h: 24 },
|
|
110
|
+
visible: true,
|
|
111
|
+
}
|
|
112
|
+
const tree: A11yNode = {
|
|
113
|
+
rawType: 'other',
|
|
114
|
+
bounds: { x: 0, y: 0, w: 100, h: 100 },
|
|
115
|
+
children: [node],
|
|
116
|
+
}
|
|
117
|
+
const runtime = new MockSimRuntime({ snapshotResult: tree })
|
|
118
|
+
const resolver = new MockSelectorResolver()
|
|
119
|
+
resolver.registerHit('{"id":"msg"}', 'msg')
|
|
120
|
+
const app = await Smix.launchApp(bundleId('dev.smix.fixture'), runtime, resolver.resolve)
|
|
121
|
+
await expect(
|
|
122
|
+
app.find(Selector.id('msg')).toContainText('Goodbye', { timeoutMs: 200 }),
|
|
123
|
+
).rejects.toMatchObject({ code: 'timeout' })
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// v7.6 c1 update: toHaveLabel + toHaveCount stubs replaced — see
|
|
128
|
+
// LocatorToHave.test.ts for the c1 replacement tests.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// v7.6 c1 — Locator.toHaveCount + toHaveLabel mock-based vitest tests.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
bundleId,
|
|
6
|
+
literal,
|
|
7
|
+
MockLabelsResolver,
|
|
8
|
+
MockSelectorResolver,
|
|
9
|
+
MockSimRuntime,
|
|
10
|
+
Selector,
|
|
11
|
+
Smix,
|
|
12
|
+
type A11yNode,
|
|
13
|
+
} from '../index.js'
|
|
14
|
+
|
|
15
|
+
describe('Locator.toHaveCount', () => {
|
|
16
|
+
test('succeeds when match count equals expected', async () => {
|
|
17
|
+
const tree: A11yNode = {
|
|
18
|
+
rawType: 'other',
|
|
19
|
+
bounds: { x: 0, y: 0, w: 100, h: 100 },
|
|
20
|
+
visible: true,
|
|
21
|
+
children: [],
|
|
22
|
+
}
|
|
23
|
+
const runtime = new MockSimRuntime({ snapshotResult: tree })
|
|
24
|
+
const resolver = new MockSelectorResolver()
|
|
25
|
+
const labelsResolver = new MockLabelsResolver()
|
|
26
|
+
labelsResolver.registerLabels('{"label":"Item"}', ['Item', 'Item', 'Item'])
|
|
27
|
+
const app = await Smix.launchApp(
|
|
28
|
+
bundleId('dev.smix.fixture'),
|
|
29
|
+
runtime,
|
|
30
|
+
resolver.resolve,
|
|
31
|
+
labelsResolver.resolve,
|
|
32
|
+
)
|
|
33
|
+
await app.find(Selector.label('Item')).toHaveCount(3, { timeoutMs: 500 })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('throws wrongState when count mismatch', async () => {
|
|
37
|
+
const tree: A11yNode = {
|
|
38
|
+
rawType: 'other',
|
|
39
|
+
bounds: { x: 0, y: 0, w: 100, h: 100 },
|
|
40
|
+
visible: true,
|
|
41
|
+
children: [],
|
|
42
|
+
}
|
|
43
|
+
const runtime = new MockSimRuntime({ snapshotResult: tree })
|
|
44
|
+
const resolver = new MockSelectorResolver()
|
|
45
|
+
const labelsResolver = new MockLabelsResolver()
|
|
46
|
+
labelsResolver.registerLabels('{"label":"Item"}', ['Item']) // 1 match, expect 3
|
|
47
|
+
const app = await Smix.launchApp(
|
|
48
|
+
bundleId('dev.smix.fixture'),
|
|
49
|
+
runtime,
|
|
50
|
+
resolver.resolve,
|
|
51
|
+
labelsResolver.resolve,
|
|
52
|
+
)
|
|
53
|
+
await expect(
|
|
54
|
+
app.find(Selector.label('Item')).toHaveCount(3, { timeoutMs: 200 }),
|
|
55
|
+
).rejects.toMatchObject({
|
|
56
|
+
name: 'ExpectationFailure',
|
|
57
|
+
code: 'wrongState',
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('Locator.toHaveLabel', () => {
|
|
63
|
+
test('succeeds when matched node label equals expected', async () => {
|
|
64
|
+
const node: A11yNode = {
|
|
65
|
+
rawType: 'button',
|
|
66
|
+
identifier: 'btn-x',
|
|
67
|
+
label: 'Submit',
|
|
68
|
+
bounds: { x: 0, y: 0, w: 10, h: 10 },
|
|
69
|
+
visible: true,
|
|
70
|
+
}
|
|
71
|
+
const tree: A11yNode = {
|
|
72
|
+
rawType: 'other',
|
|
73
|
+
bounds: { x: 0, y: 0, w: 100, h: 100 },
|
|
74
|
+
visible: true,
|
|
75
|
+
children: [node],
|
|
76
|
+
}
|
|
77
|
+
const runtime = new MockSimRuntime({ snapshotResult: tree })
|
|
78
|
+
const resolver = new MockSelectorResolver()
|
|
79
|
+
resolver.registerHit('{"id":"btn-x"}', 'btn-x')
|
|
80
|
+
const app = await Smix.launchApp(
|
|
81
|
+
bundleId('dev.smix.fixture'),
|
|
82
|
+
runtime,
|
|
83
|
+
resolver.resolve,
|
|
84
|
+
new MockLabelsResolver().resolve,
|
|
85
|
+
)
|
|
86
|
+
await app.find(Selector.id('btn-x')).toHaveLabel('Submit', { timeoutMs: 500 })
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('timeout when label never matches', async () => {
|
|
90
|
+
const node: A11yNode = {
|
|
91
|
+
rawType: 'button',
|
|
92
|
+
identifier: 'btn-x',
|
|
93
|
+
label: 'Cancel',
|
|
94
|
+
bounds: { x: 0, y: 0, w: 10, h: 10 },
|
|
95
|
+
visible: true,
|
|
96
|
+
}
|
|
97
|
+
const tree: A11yNode = {
|
|
98
|
+
rawType: 'other',
|
|
99
|
+
bounds: { x: 0, y: 0, w: 100, h: 100 },
|
|
100
|
+
visible: true,
|
|
101
|
+
children: [node],
|
|
102
|
+
}
|
|
103
|
+
const runtime = new MockSimRuntime({ snapshotResult: tree })
|
|
104
|
+
const resolver = new MockSelectorResolver()
|
|
105
|
+
resolver.registerHit('{"id":"btn-x"}', 'btn-x')
|
|
106
|
+
const app = await Smix.launchApp(
|
|
107
|
+
bundleId('dev.smix.fixture'),
|
|
108
|
+
runtime,
|
|
109
|
+
resolver.resolve,
|
|
110
|
+
new MockLabelsResolver().resolve,
|
|
111
|
+
)
|
|
112
|
+
await expect(
|
|
113
|
+
app.find(Selector.id('btn-x')).toHaveLabel('Submit', { timeoutMs: 200 }),
|
|
114
|
+
).rejects.toMatchObject({
|
|
115
|
+
code: 'timeout',
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// v7.5 c1 — MVP shape JVM vitest tests.
|
|
2
|
+
//
|
|
3
|
+
// Mirror Kotlin MvpApiShapeTest + Swift MvpApiShapeTests. Verifies
|
|
4
|
+
// the SDK exposes the right TypeScript types + constructors and that
|
|
5
|
+
// stub surface throws SmixNotImplementedError with explicit stage
|
|
6
|
+
// markers.
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from 'vitest'
|
|
9
|
+
import {
|
|
10
|
+
App,
|
|
11
|
+
ExpectationFailure,
|
|
12
|
+
FAILURE_CODES,
|
|
13
|
+
literal,
|
|
14
|
+
MockSelectorResolver,
|
|
15
|
+
MockSimRuntime,
|
|
16
|
+
Selector,
|
|
17
|
+
Smix,
|
|
18
|
+
SmixNotImplementedError,
|
|
19
|
+
appPath,
|
|
20
|
+
bundleId,
|
|
21
|
+
type A11yRole,
|
|
22
|
+
type FailureCode,
|
|
23
|
+
} from '../index.js'
|
|
24
|
+
|
|
25
|
+
describe('Selector base cases', () => {
|
|
26
|
+
test('selector base cases exhaustive', () => {
|
|
27
|
+
const cases: Selector[] = [
|
|
28
|
+
Selector.id('btn-login'),
|
|
29
|
+
Selector.text(literal('Sign In')),
|
|
30
|
+
Selector.label('Settings'),
|
|
31
|
+
Selector.role('button'),
|
|
32
|
+
Selector.role('button', literal('Submit')),
|
|
33
|
+
]
|
|
34
|
+
expect(cases.length).toBe(5)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('Selector.id encodes as {"id":...}', () => {
|
|
38
|
+
const sel = Selector.id('btn-login')
|
|
39
|
+
expect(sel.data.kind).toBe('id')
|
|
40
|
+
if (sel.data.kind === 'id') expect(sel.data.id).toBe('btn-login')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('A11yRole', () => {
|
|
45
|
+
test('camelCase wire values exposed as type union', () => {
|
|
46
|
+
const roles: A11yRole[] = [
|
|
47
|
+
'button', 'link', 'textField', 'secureTextField', 'searchField',
|
|
48
|
+
'switch', 'toggle', 'checkBox', 'radio', 'image', 'staticText',
|
|
49
|
+
'tab', 'tabBar', 'navigationBar', 'cell', 'alert', 'dialog',
|
|
50
|
+
'slider', 'progressBar', 'picker', 'menu', 'menuItem',
|
|
51
|
+
'scrollView', 'segmentedControl', 'table', 'collectionView',
|
|
52
|
+
'webView', 'keyboard',
|
|
53
|
+
]
|
|
54
|
+
expect(roles.length).toBe(28)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('FailureCode', () => {
|
|
59
|
+
test('6 cases exposed', () => {
|
|
60
|
+
expect(FAILURE_CODES.length).toBe(6)
|
|
61
|
+
const expected = new Set<FailureCode>([
|
|
62
|
+
'notFound', 'ambiguous', 'notInteractable',
|
|
63
|
+
'timeout', 'wrongState', 'unknown',
|
|
64
|
+
])
|
|
65
|
+
expect(new Set(FAILURE_CODES)).toEqual(expected)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('toJson() emits stable keys', () => {
|
|
69
|
+
const failure = new ExpectationFailure({
|
|
70
|
+
code: 'notFound',
|
|
71
|
+
message: 'no candidates',
|
|
72
|
+
selectorJson: '{"id":"btn"}',
|
|
73
|
+
suggestions: ['check id'],
|
|
74
|
+
timestamp: 1_780_000_000_000,
|
|
75
|
+
})
|
|
76
|
+
const json = failure.toJson()
|
|
77
|
+
for (const key of ['code', 'message', 'selector', 'visibleElements', 'suggestions', 'timestamp']) {
|
|
78
|
+
const count = json.split(`"${key}":`).length - 1
|
|
79
|
+
expect(count, `key '${key}' should appear exactly once`).toBe(1)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('toJson() camelCase FailureCode raw value', () => {
|
|
84
|
+
for (const code of FAILURE_CODES) {
|
|
85
|
+
const f = new ExpectationFailure({ code, message: '' })
|
|
86
|
+
expect(f.toJson()).toContain(`"${code}"`)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// v7.6 c1 update: toHaveLabel + toHaveCount stubs replaced with real
|
|
92
|
+
// wires via dedicated FFI resolve_selector_count / resolve_selector_labels.
|
|
93
|
+
// All SDK API stubs from v7.5 c1 are now wired.
|
|
94
|
+
|
|
95
|
+
describe('Smix.launchApp wire (v7.5 c1)', () => {
|
|
96
|
+
test('launchApp(.bundleId) calls runtime.launch', async () => {
|
|
97
|
+
const runtime = new MockSimRuntime()
|
|
98
|
+
await Smix.launchApp(bundleId('dev.smix.target'), runtime, new MockSelectorResolver().resolve)
|
|
99
|
+
expect(runtime.launchCalls).toEqual(['dev.smix.target'])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('launchApp(.appPath) dispatches to launchFromPath (mirror Swift v7.2 c5)', async () => {
|
|
103
|
+
const runtime = new MockSimRuntime()
|
|
104
|
+
const app = await Smix.launchApp(appPath('/tmp/Foo.apk'), runtime, new MockSelectorResolver().resolve)
|
|
105
|
+
expect(runtime.launchFromPathCalls).toEqual(['/tmp/Foo.apk'])
|
|
106
|
+
expect(runtime.launchCalls).toEqual([])
|
|
107
|
+
expect(app.bundleId).toBe('/tmp/Foo.apk')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('relaunch issues terminate + launch', async () => {
|
|
111
|
+
const runtime = new MockSimRuntime()
|
|
112
|
+
const app = await Smix.launchApp(bundleId('dev.smix.target'), runtime, new MockSelectorResolver().resolve)
|
|
113
|
+
await app.relaunch()
|
|
114
|
+
expect(runtime.launchCalls).toEqual(['dev.smix.target', 'dev.smix.target'])
|
|
115
|
+
expect(runtime.terminateCalls).toEqual(['dev.smix.target'])
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,161 @@
|
|
|
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.
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'vitest'
|
|
7
|
+
import {
|
|
8
|
+
encodeSelectorJson,
|
|
9
|
+
literal,
|
|
10
|
+
regex,
|
|
11
|
+
Selector,
|
|
12
|
+
selectorFromJsonValue,
|
|
13
|
+
selectorToJsonValue,
|
|
14
|
+
type AnchorBox,
|
|
15
|
+
} from '../index.js'
|
|
16
|
+
|
|
17
|
+
describe('Pattern wire shape', () => {
|
|
18
|
+
test('literal encodes as bare string', () => {
|
|
19
|
+
const sel = Selector.text(literal('hello'))
|
|
20
|
+
const json = encodeSelectorJson(sel)
|
|
21
|
+
expect(json).toContain('"text":"hello"')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('regex encodes as object', () => {
|
|
25
|
+
const sel = Selector.text(regex('foo.*', 'i'))
|
|
26
|
+
const json = encodeSelectorJson(sel)
|
|
27
|
+
expect(json).toContain('"regex":"foo.*"')
|
|
28
|
+
expect(json).toContain('"flags":"i"')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('Selector base wire shape', () => {
|
|
33
|
+
test('id with modifiers flatten', () => {
|
|
34
|
+
const sel = Selector.id('btn-login').below(Selector.text(literal('Sign In'))).nth(0)
|
|
35
|
+
const json = encodeSelectorJson(sel)
|
|
36
|
+
expect(json).toContain('"id":"btn-login"')
|
|
37
|
+
expect(json).toContain('"below":{"text":"Sign In"}')
|
|
38
|
+
expect(json).toContain('"nth":0')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('role with name pattern', () => {
|
|
42
|
+
const sel = Selector.role('button', literal('Submit'))
|
|
43
|
+
const json = encodeSelectorJson(sel)
|
|
44
|
+
expect(json).toContain('"role":"button"')
|
|
45
|
+
expect(json).toContain('"name":"Submit"')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('focused encodes as boolean', () => {
|
|
49
|
+
const json = encodeSelectorJson(Selector.focused())
|
|
50
|
+
expect(json).toBe('{"focused":true}')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('anchor with index', () => {
|
|
54
|
+
const box: AnchorBox = { below: Selector.text(literal('Address')) }
|
|
55
|
+
const sel = Selector.anchor(box, { nth: 2 })
|
|
56
|
+
const json = encodeSelectorJson(sel)
|
|
57
|
+
expect(json).toContain('"anchor":{"below":{"text":"Address"}}')
|
|
58
|
+
expect(json).toContain('"nth":2')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('localizedText carries multi-locale map', () => {
|
|
62
|
+
const sel = Selector.localizedText({ en: 'Submit', ja: '送信' })
|
|
63
|
+
const json = encodeSelectorJson(sel)
|
|
64
|
+
expect(json).toContain('"localizedText"')
|
|
65
|
+
expect(json).toContain('Submit')
|
|
66
|
+
expect(json).toContain('送信')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('Wire decode roundtrip', () => {
|
|
71
|
+
test('id with below and nth', () => {
|
|
72
|
+
const wire = { id: 'btn-login', below: { text: 'Sign In' }, nth: 0 }
|
|
73
|
+
const sel = selectorFromJsonValue(wire)
|
|
74
|
+
expect(sel.data.kind).toBe('id')
|
|
75
|
+
if (sel.data.kind === 'id') {
|
|
76
|
+
expect(sel.data.id).toBe('btn-login')
|
|
77
|
+
expect(sel.data.modifiers.nth).toBe(0)
|
|
78
|
+
const below = sel.data.modifiers.below
|
|
79
|
+
expect(below?.data.kind).toBe('text')
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('focused', () => {
|
|
84
|
+
const sel = selectorFromJsonValue({ focused: true })
|
|
85
|
+
expect(sel.data.kind).toBe('focused')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('anchor', () => {
|
|
89
|
+
const sel = selectorFromJsonValue({ anchor: { below: { text: 'Address' } }, nth: 2 })
|
|
90
|
+
expect(sel.data.kind).toBe('anchor')
|
|
91
|
+
if (sel.data.kind === 'anchor') {
|
|
92
|
+
expect(sel.data.index.nth).toBe(2)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('localizedText', () => {
|
|
97
|
+
const sel = selectorFromJsonValue({ localizedText: { en: 'Submit', ja: '送信' } })
|
|
98
|
+
expect(sel.data.kind).toBe('localizedText')
|
|
99
|
+
if (sel.data.kind === 'localizedText') {
|
|
100
|
+
expect(sel.data.map.en).toBe('Submit')
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('role with regex pattern', () => {
|
|
105
|
+
const sel = selectorFromJsonValue({ role: 'button', name: { regex: '^Sub', flags: 'i' } })
|
|
106
|
+
expect(sel.data.kind).toBe('role')
|
|
107
|
+
if (sel.data.kind === 'role') {
|
|
108
|
+
expect(sel.data.role).toBe('button')
|
|
109
|
+
expect(sel.data.name?.kind).toBe('regex')
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('Fluent chaining', () => {
|
|
115
|
+
test('below sets modifier', () => {
|
|
116
|
+
const sel = Selector.id('btn').below(Selector.text(literal('anchor')))
|
|
117
|
+
if (sel.data.kind === 'id') {
|
|
118
|
+
expect(sel.data.modifiers.below).toBeDefined()
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('chained multiple', () => {
|
|
123
|
+
const sel = Selector.id('btn')
|
|
124
|
+
.below(Selector.text(literal('address')))
|
|
125
|
+
.above(Selector.text(literal('title')))
|
|
126
|
+
.nth(3)
|
|
127
|
+
if (sel.data.kind === 'id') {
|
|
128
|
+
expect(sel.data.modifiers.below).toBeDefined()
|
|
129
|
+
expect(sel.data.modifiers.above).toBeDefined()
|
|
130
|
+
expect(sel.data.modifiers.nth).toBe(3)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('first and last', () => {
|
|
135
|
+
const f = Selector.label('Item').first()
|
|
136
|
+
const l = Selector.label('Item').last()
|
|
137
|
+
if (f.data.kind === 'label') expect(f.data.modifiers.first).toBe(true)
|
|
138
|
+
if (l.data.kind === 'label') expect(l.data.modifiers.last).toBe(true)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('Roundtrip with modifiers', () => {
|
|
143
|
+
test('id with multiple modifiers byte-identical roundtrip', () => {
|
|
144
|
+
const original = Selector.id('btn-foo')
|
|
145
|
+
.below(Selector.text(literal('Address')))
|
|
146
|
+
.nth(0)
|
|
147
|
+
const json = JSON.stringify(selectorToJsonValue(original))
|
|
148
|
+
const decoded = selectorFromJsonValue(JSON.parse(json))
|
|
149
|
+
const reencoded = JSON.stringify(selectorToJsonValue(decoded))
|
|
150
|
+
expect(reencoded).toBe(json)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('role with modifiers and name roundtrip', () => {
|
|
154
|
+
const original = Selector.role('button', literal('Submit'))
|
|
155
|
+
.below(Selector.text(literal('Email')))
|
|
156
|
+
const json = JSON.stringify(selectorToJsonValue(original))
|
|
157
|
+
const decoded = selectorFromJsonValue(JSON.parse(json))
|
|
158
|
+
const reencoded = JSON.stringify(selectorToJsonValue(decoded))
|
|
159
|
+
expect(reencoded).toBe(json)
|
|
160
|
+
})
|
|
161
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// v7.5 c1 — @smix/rn public API exports.
|
|
2
|
+
//
|
|
3
|
+
// Mirror Kotlin SDK + Swift SDK surface. Per iron rule §9 #13:
|
|
4
|
+
// this package only ships to test harnesses (vitest / jest / Detox-like
|
|
5
|
+
// runners) — never bundled into a production RN app.
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
App,
|
|
9
|
+
} from './App.js'
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
literal,
|
|
13
|
+
patternFromJson,
|
|
14
|
+
patternToJson,
|
|
15
|
+
regex,
|
|
16
|
+
type Pattern,
|
|
17
|
+
} from './Pattern.js'
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
encodeSelectorJson,
|
|
21
|
+
Selector,
|
|
22
|
+
selectorFromJsonValue,
|
|
23
|
+
selectorToJsonValue,
|
|
24
|
+
type AnchorBox,
|
|
25
|
+
type IndexModifiers,
|
|
26
|
+
type Modifiers,
|
|
27
|
+
type SelectorData,
|
|
28
|
+
type SelectorKind,
|
|
29
|
+
} from './Selector.js'
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
ExpectationFailure,
|
|
33
|
+
FAILURE_CODES,
|
|
34
|
+
type FailureCode,
|
|
35
|
+
} from './ExpectationFailure.js'
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
Locator,
|
|
39
|
+
SmixNotImplementedError,
|
|
40
|
+
} from './Locator.js'
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
appPath,
|
|
44
|
+
bundleId,
|
|
45
|
+
Smix,
|
|
46
|
+
type AppTarget,
|
|
47
|
+
} from './Smix.js'
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
MockSimRuntime,
|
|
51
|
+
type KeyName,
|
|
52
|
+
type LaunchFreshCall,
|
|
53
|
+
type SmixSimRuntime,
|
|
54
|
+
type SwipeDirection,
|
|
55
|
+
} from './SimRuntime.js'
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
MockLabelsResolver,
|
|
59
|
+
MockSelectorResolver,
|
|
60
|
+
type LabelsResolver,
|
|
61
|
+
type SelectorResolver,
|
|
62
|
+
} from './SelectorResolver.js'
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
HttpSimRuntime,
|
|
66
|
+
type HttpFetch,
|
|
67
|
+
} from './HttpRunner.js'
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
findById,
|
|
71
|
+
flatten,
|
|
72
|
+
rectCenter,
|
|
73
|
+
type A11yNode,
|
|
74
|
+
type A11yRole,
|
|
75
|
+
type Rect,
|
|
76
|
+
} from './A11yNode.js'
|