also looking at this
feat(maps): add key-free native Android maps and shared map controls
#363
28 files
+1857
-481
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -94,6 +94,21 @@ describe('package-owned iOS shells', () => { | ||
| 94 | 94 | expect(handoff).not.toContain('target.launch()') |
| 95 | 95 | }) |
| 96 | 96 | |
| 97 | it('checks actual native field values after a real edit-menu gesture', () => { | |
| 98 | expect(serializeSimulatorSteps([ | |
| 99 | { command: 'longpressid', text: 'route-name' }, | |
| 100 | { command: 'tapexact', text: 'Paste' }, | |
| 101 | { command: 'valueid', id: 'route-name', text: 'https://example.org/trail/7' }, | |
| 102 | ])).toBe('longpressid route-name\ntapexact Paste\nvalueid route-name https://example.org/trail/7\n') | |
| 103 | expect(() => serializeSimulatorSteps([{ command: 'longpressid', text: 'two fields' }])).toThrow() | |
| 104 | expect(() => serializeSimulatorSteps([{ command: 'valueid', id: 'route-name', text: 'x\nlaunch' }])).toThrow() | |
| 105 | const output = folder() | |
| 106 | initSimulatorDriver({ output, bundleId: 'org.example.app' }) | |
| 107 | const source = readFileSync(join(output, 'UITests/DriverTests.swift'), 'utf8') | |
| 108 | expect(source).toContain('target.press(forDuration: 1.2)') | |
| 109 | expect(source).toContain('(target.value as? String) == want') | |
| 110 | }) | |
| 111 | ||
| 97 | 112 | it('bundles an on-device journey with native XCTest evidence and removes it when returning to server mode', () => { |
| 98 | 113 | const output = folder() |
| 99 | 114 | const options = { output, bundleId: 'org.example.app', script: 'activate\nshot current\ntree current\n' } |
| @@ -155,7 +170,7 @@ describe('package-owned iOS shells', () => { | ||
| 155 | 170 | initSimulatorDriver({ output, bundleId: 'org.example.app' }) |
| 156 | 171 | const source = readFileSync(join(output, 'UITests/DriverTests.swift'), 'utf8') |
| 157 | 172 | const handled = new Set([...source.matchAll(/case "([a-z]+)"/g)].map(match => match[1])) |
| 158 | const commands: SimulatorStep['command'][] = ['launch', 'activate', 'terminate', 'home', 'back', 'return', 'unlabelled', 'background', 'wait', 'delay', 'net', 'swipe', 'drag', 'edge', 'tabbar', 'tapxy', 'touches', 'field', 'typeid', 'labelid', 'expect', 'expectexact', 'expectid', 'tap', 'tapif', 'tapid', 'tapexact', 'type', 'absent', 'absentid', 'selected', 'tab', 'scrollto', 'dismiss', 'log', 'shot', 'tree', 'audit', 'exec', 'openurl', 'universallink', 'ifid', 'notifications', 'systemexpect', 'systemabsent', 'systemtap', 'systemtree'] | |
| 173 | const commands: SimulatorStep['command'][] = ['launch', 'activate', 'terminate', 'home', 'back', 'return', 'unlabelled', 'background', 'wait', 'delay', 'net', 'swipe', 'drag', 'edge', 'tabbar', 'tapxy', 'touches', 'field', 'typeid', 'labelid', 'valueid', 'sliderid', 'expectapp', 'expect', 'expectexact', 'expectid', 'tap', 'tapif', 'tapid', 'longpressid', 'tapexact', 'type', 'absent', 'absentid', 'selected', 'tab', 'scrollto', 'dismiss', 'log', 'shot', 'tree', 'audit', 'exec', 'openurl', 'universallink', 'ifid', 'notifications', 'systemexpect', 'systemabsent', 'systemtap', 'systemtree'] | |
| 159 | 174 | expect(commands.filter(command => !handled.has(command))).toEqual([]) |
| 160 | 175 | // ifid re-enters the same dispatcher, so a guarded command is any command. |
| 161 | 176 | expect(source).toContain('func run(_ line: String)') |
| @@ -90,13 +90,13 @@ export type SimulatorStep = | ||
| 90 | 90 | /** One native event record, without quiescence between paths. Times are seconds. */ |
| 91 | 91 | | { command: 'touches', paths: TimedTouchPoint[][] } |
| 92 | 92 | | { command: 'field', index: number, text: string } |
| 93 | | { command: 'typeid' | 'labelid', id: string, text: string } | |
| 93 | | { command: 'typeid' | 'labelid' | 'valueid', id: string, text: string } | |
| 94 | 94 | /** Adjust an actual native UISlider by identifier, using its normalized range. */ |
| 95 | 95 | | { command: 'sliderid', id: string, position: number } |
| 96 | 96 | /** Assert a handoff reached the foreground application without launching it. */ |
| 97 | 97 | | { command: 'expectapp', text: string, seconds?: number } |
| 98 | 98 | | { command: 'expect' | 'expectexact' | 'expectid' | 'systemexpect', text: string, seconds?: number } |
| 99 | | { command: 'tap' | 'tapif' | 'tapid' | 'tapexact' | 'type' | 'absent' | 'absentid' | 'selected' | 'tab' | 'scrollto' | 'dismiss' | 'log' | 'systemtap' | 'systemabsent', text: string } | |
| 99 | | { command: 'tap' | 'tapif' | 'tapid' | 'longpressid' | 'tapexact' | 'type' | 'absent' | 'absentid' | 'selected' | 'tab' | 'scrollto' | 'dismiss' | 'log' | 'systemtap' | 'systemabsent', text: string } | |
| 100 | 100 | | { command: 'shot' | 'tree' | 'audit' | 'exec' | 'systemtree', name: string } |
| 101 | 101 | | { command: 'openurl', url: string } |
| 102 | 102 | /** Use Apple's universalLinksOnly API, allowing association downloads up to 60 seconds. */ |
| @@ -156,6 +156,7 @@ export function serializeSimulatorSteps(steps: readonly SimulatorStep[]): string | ||
| 156 | 156 | else if ('id' in step) arg = `${token(step.id)} ${step.text}` |
| 157 | 157 | else if ('text' in step) { |
| 158 | 158 | arg = step.text |
| 159 | if (step.command === 'longpressid') token(arg) | |
| 159 | 160 | if (step.command === 'expectapp' && !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(arg)) |
| 160 | 161 | throw new Error('Expected an application bundle identifier') |
| 161 | 162 | if ('seconds' in step && step.seconds !== undefined) { |
| @@ -21,6 +21,7 @@ import Darwin | ||
| 21 | 21 | /// tap <text> first hittable element whose label contains text |
| 22 | 22 | /// tapif <text> tap it when it is on screen, else carry on (the big timer's "Show the controls") |
| 23 | 23 | /// tapid <identifier> |
| 24 | /// longpressid <identifier> holds a hittable native element to open its edit menu | |
| 24 | 25 | /// touches <JSON> timed normalized touch paths in one native dispatch |
| 25 | 26 | /// tapxy <x> <y> normalised 0…1 of the screen |
| 26 | 27 | /// tapexact <label> a button whose label is exactly this |
| @@ -36,6 +37,7 @@ import Darwin | ||
| 36 | 37 | /// absent <text> logs PASS when no element contains text |
| 37 | 38 | /// expectid <id> [| secs] / absentid <id> the same by accessibility identifier (testID) |
| 38 | 39 | /// labelid <id> <text> that element's label is exactly text (a count, a status) |
| 40 | /// valueid <id> <text> that element's actual string value is exactly text | |
| 39 | 41 | /// tabbar shown|hidden the native tab bar (Today, Calendar, …) can be tapped, or not |
| 40 | 42 | /// selected <tab> the tab bar's selected button is this one |
| 41 | 43 | /// tab <name> tap a tab bar button |
| @@ -292,6 +294,14 @@ final class DriverTests: XCTestCase { | ||
| 292 | 294 | let target = identified(arg) |
| 293 | 295 | if target.waitForExistence(timeout: 10) { press(target); note("tapid \(arg): ok") } |
| 294 | 296 | else { XCTFail("Element not found"); note("FAIL tapid \(arg): NOT FOUND") } |
| 297 | case "longpressid": | |
| 298 | let target = identified(arg) | |
| 299 | guard target.waitForExistence(timeout: 10), target.isHittable else { | |
| 300 | check(false, "longpressid \(arg) is hittable") | |
| 301 | return | |
| 302 | } | |
| 303 | target.press(forDuration: 1.2) | |
| 304 | note("longpressid \(arg): ok") | |
| 295 | 305 | case "touches": |
| 296 | 306 | do { |
| 297 | 307 | guard let data = arg.data(using: .utf8), |
| @@ -404,6 +414,14 @@ final class DriverTests: XCTestCase { | ||
| 404 | 414 | let want = idParts.count > 1 ? idParts[1] : "" |
| 405 | 415 | let seen = target.waitForExistence(timeout: 5) ? target.label : "(missing)" |
| 406 | 416 | check(seen == want, "labelid \(idParts[0]) \(want) (saw \(seen))") |
| 417 | case "valueid": | |
| 418 | let idParts = arg.split(separator: " ", maxSplits: 1).map(String.init) | |
| 419 | let target = identified(idParts[0]) | |
| 420 | let want = idParts.count > 1 ? idParts[1] : "" | |
| 421 | let matched = poll(5) { | |
| 422 | target.exists && (target.value as? String) == want ? target : nil | |
| 423 | } | |
| 424 | check(matched != nil, "valueid \(idParts[0]) \(want)") | |
| 407 | 425 | case "absentid": |
| 408 | 426 | check(!identified(arg).exists, "absentid \(arg)") |
| 409 | 427 | case "absent": |