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.
| @@ -76,6 +76,24 @@ describe('package-owned iOS shells', () => { | ||
| 76 | 76 | } |
| 77 | 77 | }) |
| 78 | 78 | |
| 79 | it('serializes native slider interactions and verifies app handoffs without launching another app', () => { | |
| 80 | expect(serializeSimulatorSteps([ | |
| 81 | { command: 'sliderid', id: 'elevation', position: 0.5 }, | |
| 82 | { command: 'expectapp', text: 'com.apple.Maps', seconds: 10 }, | |
| 83 | ])).toBe('sliderid elevation 0.5\nexpectapp com.apple.Maps | 10\n') | |
| 84 | for (const position of [-1, 1.1, NaN, Infinity]) | |
| 85 | expect(() => serializeSimulatorSteps([{ command: 'sliderid', id: 'elevation', position }])).toThrow() | |
| 86 | expect(() => serializeSimulatorSteps([{ command: 'sliderid', id: 'two sliders', position: 0.5 }])).toThrow() | |
| 87 | expect(() => serializeSimulatorSteps([{ command: 'expectapp', text: 'Maps\nlaunch' }])).toThrow() | |
| 88 | const output = folder() | |
| 89 | initSimulatorDriver({ output, bundleId: 'org.example.app' }) | |
| 90 | const source = readFileSync(join(output, 'UITests/DriverTests.swift'), 'utf8') | |
| 91 | expect(source).toContain('slider.adjust(toNormalizedSliderPosition: CGFloat(position))') | |
| 92 | const handoff = source.slice(source.indexOf('case "expectapp":'), source.indexOf('case "field":')) | |
| 93 | expect(handoff).toContain('target.wait(for: .runningForeground') | |
| 94 | expect(handoff).not.toContain('target.launch()') | |
| 95 | }) | |
| 96 | ||
| 79 | 97 | it('bundles an on-device journey with native XCTest evidence and removes it when returning to server mode', () => { |
| 80 | 98 | const output = folder() |
| 81 | 99 | const options = { output, bundleId: 'org.example.app', script: 'activate\nshot current\ntree current\n' } |
| @@ -91,6 +91,10 @@ export type SimulatorStep = | ||
| 91 | 91 | | { command: 'touches', paths: TimedTouchPoint[][] } |
| 92 | 92 | | { command: 'field', index: number, text: string } |
| 93 | 93 | | { command: 'typeid' | 'labelid', id: string, text: string } |
| 94 | /** Adjust an actual native UISlider by identifier, using its normalized range. */ | |
| 95 | | { command: 'sliderid', id: string, position: number } | |
| 96 | /** Assert a handoff reached the foreground application without launching it. */ | |
| 97 | | { command: 'expectapp', text: string, seconds?: number } | |
| 94 | 98 | | { command: 'expect' | 'expectexact' | 'expectid' | 'systemexpect', text: string, seconds?: number } |
| 95 | 99 | | { command: 'tap' | 'tapif' | 'tapid' | 'tapexact' | 'type' | 'absent' | 'absentid' | 'selected' | 'tab' | 'scrollto' | 'dismiss' | 'log' | 'systemtap' | 'systemabsent', text: string } |
| 96 | 100 | | { command: 'shot' | 'tree' | 'audit' | 'exec' | 'systemtree', name: string } |
| @@ -145,9 +149,15 @@ export function serializeSimulatorSteps(steps: readonly SimulatorStep[]): string | ||
| 145 | 149 | if (!Number.isInteger(number(step.index))) throw new Error('field index must be an integer') |
| 146 | 150 | arg = `${step.index} ${step.text}` |
| 147 | 151 | } |
| 152 | else if ('position' in step) { | |
| 153 | if (number(step.position) > 1) throw new Error('Slider position must be between 0 and 1') | |
| 154 | arg = `${token(step.id)} ${step.position}` | |
| 155 | } | |
| 148 | 156 | else if ('id' in step) arg = `${token(step.id)} ${step.text}` |
| 149 | 157 | else if ('text' in step) { |
| 150 | 158 | arg = step.text |
| 159 | if (step.command === 'expectapp' && !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(arg)) | |
| 160 | throw new Error('Expected an application bundle identifier') | |
| 151 | 161 | if ('seconds' in step && step.seconds !== undefined) { |
| 152 | 162 | if (arg.includes('|')) throw new Error('A timed expectation cannot contain |') |
| 153 | 163 | arg += ` | ${number(step.seconds)}` |
| @@ -29,6 +29,8 @@ import Darwin | ||
| 29 | 29 | /// return the keyboard's return key (Go on a sign-in form) |
| 30 | 30 | /// field <n> <text> tap the n-th text field (0-based, secure fields after plain) and type |
| 31 | 31 | /// typeid <identifier> <text> tap the element with this identifier and type |
| 32 | /// sliderid <identifier> <0…1> adjusts a hittable native slider | |
| 33 | /// expectapp <bundle-id> [| secs] asserts a native app handoff reached the foreground | |
| 32 | 34 | /// expect <text> [| secs] waits for an element containing text; logs PASS/FAIL |
| 33 | 35 | /// expectexact <label> [| secs] a button or text whose label is exactly this |
| 34 | 36 | /// absent <text> logs PASS when no element contains text |
| @@ -360,6 +362,23 @@ final class DriverTests: XCTestCase { | ||
| 360 | 362 | app.typeText(idParts.count > 1 ? idParts[1] : "") |
| 361 | 363 | note("typeid \(idParts[0]): ok") |
| 362 | 364 | } else { XCTFail("Element not found"); note("FAIL typeid \(idParts[0]): NOT FOUND") } |
| 365 | case "sliderid": | |
| 366 | let parts = arg.split(separator: " ", maxSplits: 1).map(String.init) | |
| 367 | guard parts.count == 2, let position = Double(parts[1]), position.isFinite, (0...1).contains(position) else { | |
| 368 | check(false, "sliderid requires an identifier and normalized position") | |
| 369 | return | |
| 370 | } | |
| 371 | let slider = app.sliders.matching(NSPredicate(format: "identifier == %@", parts[0])).firstMatch | |
| 372 | guard slider.waitForExistence(timeout: 10), slider.isHittable else { | |
| 373 | check(false, "sliderid \(parts[0]) is hittable") | |
| 374 | return | |
| 375 | } | |
| 376 | slider.adjust(toNormalizedSliderPosition: CGFloat(position)) | |
| 377 | note("sliderid \(parts[0]): \(position)") | |
| 378 | case "expectapp": | |
| 379 | let (bundle, seconds) = split(arg) | |
| 380 | let target = XCUIApplication(bundleIdentifier: bundle) | |
| 381 | check(target.wait(for: .runningForeground, timeout: seconds), "expectapp \(bundle)") | |
| 363 | 382 | case "field": |
| 364 | 383 | let fieldParts = arg.split(separator: " ", maxSplits: 1).map(String.init) |
| 365 | 384 | let index = Int(fieldParts[0]) ?? 0 |