ReviewOS

also looking at this

craft-native/craft

fix(mobile): preserve native editing, navigation, and durable media requests

#369
Merged chris wants to merge codex/native-vertical-scroll-tab-swipes into main
36 files +864 -185

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.

packages/ios/scripts/native-navigation-groups.tsmodified+10-0
Changes to packages/ios/scripts/native-navigation-groups.ts
@@ -11,3 +11,13 @@ export function nativeNavigationGroup(source: string, group: string): string[] {
1111 if (!selected.length) throw new Error(`Empty native navigation UI group: ${group}`)
1212 return selected.map(name => `NativeNavigationUITests/NativeNavigationUITests/${name}`)
1313}
14
15/** An ignored XCTest selector must not silently reduce required coverage. */
16export function verifyNativeNavigationResults(counts: Record<string, unknown>, expectedTestCount?: number): number {
17 const count = counts.totalTestCount
18 if (counts.result !== 'Passed' || typeof count !== 'number' || !Number.isInteger(count) || count < 1
19 || counts.passedTests !== count || counts.failedTests !== 0 || counts.skippedTests !== 0
20 || (expectedTestCount !== undefined && count !== expectedTestCount))
21 throw new Error(`Expected complete native navigation coverage (${expectedTestCount ?? 'nonempty'} tests): ${JSON.stringify(counts)}`)
22 return count
23}
packages/ios/scripts/test-native-navigation.tsmodified+6-6
Changes to packages/ios/scripts/test-native-navigation.ts
@@ -2,7 +2,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from
22import { dirname, join, resolve } from 'node:path'
33import { bootSimulator, build, init, pickSimulator } from '../src/index'
44import { addSchemeTestTargets, insertProjectTargetsBeforeSchemes } from './insert-project-targets'
5import { nativeNavigationGroup } from './native-navigation-groups'
5import { nativeNavigationGroup, verifyNativeNavigationResults } from './native-navigation-groups'
66import { createTempDir, keepTempDir, removeTempDir } from '../../typescript/src/temp-dir'
77
88const fixture = join(import.meta.dir, '..', 'fixtures', 'native-navigation')
@@ -20,7 +20,7 @@ function run(args: string[], cwd: string): void {
2020 throw new Error(`${args[0]} exited with ${result.exitCode}`)
2121}
2222
23function runXcodeTests(args: string[], cwd: string, resultBundle: string): void {
23function runXcodeTests(args: string[], cwd: string, resultBundle: string, expectedTestCount?: number): void {
2424 console.log(args.join(' '))
2525 const result = Bun.spawnSync(args, { cwd, stdout: 'inherit', stderr: 'inherit' })
2626 const evidence = process.env.CRAFT_NATIVE_NAVIGATION_RESULT
@@ -29,8 +29,7 @@ function runXcodeTests(args: string[], cwd: string, resultBundle: string): void
2929 if (summary.exitCode !== 0) throw new Error(`Could not read XCTest results: ${summary.stderr.toString()}`)
3030 const counts = JSON.parse(summary.stdout.toString())
3131 if (evidence) writeFileSync(`${resolve(evidence)}.summary.json`, summary.stdout)
32 if (counts.result !== 'Passed' || !Number.isInteger(counts.totalTestCount) || counts.totalTestCount < 1 || counts.passedTests !== counts.totalTestCount || counts.failedTests !== 0 || counts.skippedTests !== 0)
33 throw new Error(`Expected passed native navigation tests with no empty selection or skips: ${JSON.stringify(counts)}`)
32 verifyNativeNavigationResults(counts, expectedTestCount)
3433 console.log(`XCTest verified ${counts.passedTests} passed tests, zero failures and skips`)
3534 return
3635 }
@@ -144,7 +143,8 @@ try {
144143 const selectedTarget = process.env.CRAFT_ONLY_TESTING
145144 const group = process.env.CRAFT_NATIVE_NAVIGATION_GROUP
146145 if (group && (selectedTarget || selectedTest)) throw new Error('A required native UI group cannot be combined with focused selectors')
147 const selection = group ? nativeNavigationGroup(readFileSync(join(fixture, 'NativeNavigationUITests.swift'), 'utf8'), group).map(target => `-only-testing:${target}`) : selectedTarget ? selectedTarget.split(',').map(target => target.trim()).filter(Boolean).map(target => `-only-testing:${target}`) : selectedTest
146 const groupCases = group ? nativeNavigationGroup(readFileSync(join(fixture, 'NativeNavigationUITests.swift'), 'utf8'), group) : undefined
147 const selection = groupCases ? groupCases.map(target => `-only-testing:${target}`) : selectedTarget ? selectedTarget.split(',').map(target => target.trim()).filter(Boolean).map(target => `-only-testing:${target}`) : selectedTest
148148 ? [`-only-testing:NativeNavigationUITests/NativeNavigationUITests/${selectedTest}`]
149149 : []
150150 const xcodeArgs = [
@@ -162,7 +162,7 @@ try {
162162 '-resultBundlePath', resultBundle,
163163 ...selection,
164164 'test-without-building',
165 ], output, resultBundle)
165 ], output, resultBundle, groupCases?.length)
166166 console.log('Native navigation simulator tests passed')
167167 }
168168}
packages/ios/src/navigation-groups.test.tsmodified+15-1
Changes to packages/ios/src/navigation-groups.test.ts
@@ -1,7 +1,7 @@
11import { expect, test } from 'bun:test'
22import { readFileSync } from 'node:fs'
33import { join } from 'node:path'
4import { nativeNavigationGroup } from '../scripts/native-navigation-groups'
4import { nativeNavigationGroup, verifyNativeNavigationResults } from '../scripts/native-navigation-groups'
55
66const source = readFileSync(join(import.meta.dir, '../fixtures/native-navigation/NativeNavigationUITests.swift'), 'utf8')
77
@@ -27,3 +27,17 @@ test('invalid, empty or ambiguous native UI coverage fails closed', () => {
2727 expect(() => nativeNavigationGroup(source.replace('func testPushNativeBackSwipeReplaceAndRetainedHomeState', 'func noControlsJourney'), 'controls')).toThrow('coverage')
2828 expect(() => nativeNavigationGroup(`${source}\nfunc testTabSwipeSwitchesNativeRootsAndReturns() {}`, 'core')).toThrow('coverage')
2929})
30
31test('XCTest must execute every selected case without failures or skips', () => {
32 const expected = nativeNavigationGroup(source, 'core').length
33 const passed = { result: 'Passed', totalTestCount: expected, passedTests: expected, failedTests: 0, skippedTests: 0 }
34 expect(verifyNativeNavigationResults(passed, expected)).toBe(expected)
35 expect(() => verifyNativeNavigationResults({ ...passed, totalTestCount: expected - 1, passedTests: expected - 1 }, expected)).toThrow('complete')
36 for (const incomplete of [
37 { ...passed, result: 'Failed' },
38 { ...passed, totalTestCount: 0, passedTests: 0 },
39 { ...passed, passedTests: expected - 1 },
40 { ...passed, failedTests: 1 },
41 { ...passed, skippedTests: 1 },
42 ]) expect(() => verifyNativeNavigationResults(incomplete, expected)).toThrow('complete')
43})