ReviewOS

also looking at this

stacks/wildloop

feat: native trail maps and complete native mobile screens

#1017
Draft chris wants to merge codex/trail-experience into main
60 files +2505 -823

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.

.github/workflows/mobile-e2e.ymlmodified+54-0
Changes to .github/workflows/mobile-e2e.yml
@@ -8,6 +8,7 @@ on:
88 - '.maestro/**'
99 - 'tests/native/**'
1010 - 'scripts/run-native-trail-e2e.ts'
11 - 'scripts/run-native-trail-android-e2e.ts'
1112 - 'app/**'
1213 - 'config/mobile.ts'
1314 # The app icon config/mobile.ts points at; a bad one fails the iOS build.
@@ -21,6 +22,7 @@ on:
2122 - '.maestro/**'
2223 - 'tests/native/**'
2324 - 'scripts/run-native-trail-e2e.ts'
25 - 'scripts/run-native-trail-android-e2e.ts'
2426 - 'app/**'
2527 - 'config/mobile.ts'
2628 # The app icon config/mobile.ts points at; a bad one fails the iOS build.
@@ -175,3 +177,55 @@ jobs:
175177 storage/framework/runtime/e2e/native-trail/*.png
176178 storage/framework/runtime/e2e/native-trail/results-*.xcresult
177179 if-no-files-found: warn
180
181 native-trail-android:
182 runs-on: ubuntu-latest
183 timeout-minutes: 35
184 env:
185 QA_PORT_BASE: '7320'
186 NATIVE_QA_ANDROID_DEVICE: emulator-5554
187 steps:
188 - uses: actions/checkout@v5
189 - uses: oven-sh/setup-bun@v2
190 with:
191 bun-version: '1.4.2'
192 - uses: actions/setup-java@v5
193 with:
194 distribution: temurin
195 java-version: '17'
196 - uses: gradle/actions/setup-gradle@v4
197 with:
198 gradle-version: '8.11.1'
199 - name: Install Maestro
200 run: |
201 curl -fsSL "https://github.com/mobile-dev-inc/Maestro/releases/download/${MAESTRO_VERSION}/maestro.zip" -o "$RUNNER_TEMP/maestro.zip"
202 echo "$MAESTRO_SHA256 $RUNNER_TEMP/maestro.zip" | sha256sum --check
203 unzip -q "$RUNNER_TEMP/maestro.zip" -d "$RUNNER_TEMP/maestro-cli"
204 echo "$RUNNER_TEMP/maestro-cli/maestro/bin" >> "$GITHUB_PATH"
205 - name: Install published dependencies
206 run: bun install --frozen-lockfile
207 - name: Build native MapLibre trail app
208 run: bun scripts/run-native-trail-android-e2e.ts --build-only
209 - name: Enable KVM
210 run: |
211 echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
212 sudo udevadm control --reload-rules
213 sudo udevadm trigger --name-match=kvm
214 - name: Test native maps, sticky actions and persisted reviews
215 uses: reactivecircus/android-emulator-runner@v2
216 with:
217 api-level: 34
218 target: default
219 arch: x86_64
220 profile: pixel_7
221 ram-size: 4096M
222 emulator-options: -no-window -no-snapshot -gpu swiftshader_indirect -noaudio -no-boot-anim
223 disable-animations: false
224 script: bun scripts/run-native-trail-android-e2e.ts --skip-build
225 - name: Upload Android native trail evidence
226 if: always()
227 uses: actions/upload-artifact@v5
228 with:
229 name: native-trail-android
230 path: storage/framework/runtime/e2e/native-trail-android
231 if-no-files-found: warn
.maestro/native-trail/maps.yamladded+71-0
Changes to .maestro/native-trail/maps.yaml
@@ -0,0 +1,71 @@
1appId: ${APP_ID}
2name: Native Android trail maps and sticky save
3---
4- runFlow: sign-in.yaml
5- tapOn:
6 id: trail-save
7- extendedWaitUntil:
8 visible: '^Saved$'
9 timeout: 15000
10- tapOn:
11 id: trail-save
12- extendedWaitUntil:
13 visible: '^Save$'
14 timeout: 15000
15- scrollUntilVisible:
16 element:
17 id: trail-map-fullscreen
18 direction: DOWN
19- assertVisible:
20 id: trail-sticky-actions
21- tapOn:
22 id: trail-map-fullscreen
23- extendedWaitUntil:
24 visible:
25 id: trail-full-map
26 timeout: 30000
27- takeScreenshot: android-native-map-outdoors
28- tapOn:
29 id: map-zoom-in
30- tapOn:
31 id: map-zoom-out
32- tapOn:
33 id: map-fit
34- tapOn:
35 id: map-3d
36- assertVisible: '^2D$'
37- tapOn:
38 id: map-north
39- tapOn:
40 id: map-profile-toggle
41- assertVisible: 'Toggle elevation profile, Elevation profile collapsed'
42- takeScreenshot: android-native-map-profile-collapsed
43- tapOn:
44 id: map-profile-toggle
45- assertVisible: 'Toggle elevation profile, Elevation profile expanded'
46- tapOn:
47 id: map-layer
48- tapOn: '^Satellite$'
49- assertVisible: 'Map layer, Satellite'
50- takeScreenshot: android-native-map-satellite
51- tapOn:
52 id: map-layer
53- tapOn: '^Hybrid$'
54- assertVisible: 'Map layer, Hybrid'
55- takeScreenshot: android-native-map-hybrid
56- tapOn:
57 id: map-layer
58- tapOn: '^Street$'
59- assertVisible: 'Map layer, Street'
60- takeScreenshot: android-native-map-street
61- tapOn:
62 id: map-layer
63- tapOn: '^Outdoors$'
64- assertVisible: 'Map layer, Outdoors'
65- tapOn:
66 id: map-fit
67- takeScreenshot: android-native-map-outdoors-restored
68- back
69- assertVisible:
70 id: trail-sticky-actions
71- takeScreenshot: android-native-sticky-actions
.maestro/native-trail/review-create.yamladded+37-0
Changes to .maestro/native-trail/review-create.yaml
@@ -0,0 +1,37 @@
1appId: ${APP_ID}
2name: Create a review with native controls
3---
4- runFlow: sign-in.yaml
5- scrollUntilVisible:
6 element:
7 id: trail-write-review
8 direction: DOWN
9- assertVisible:
10 id: trail-sticky-actions
11- tapOn:
12 id: trail-write-review
13- extendedWaitUntil:
14 visible:
15 id: trail-review-content
16 timeout: 15000
17- tapOn:
18 id: trail-review-star-5
19- tapOn:
20 id: trail-review-content
21- inputText: ${REVIEW_TEXT}
22- hideKeyboard
23- scrollUntilVisible:
24 element:
25 id: trail-review-submit
26 direction: DOWN
27- tapOn:
28 id: trail-review-submit
29- extendedWaitUntil:
30 visible: 'Edit your review'
31 timeout: 15000
32- scrollUntilVisible:
33 element:
34 text: ${REVIEW_TEXT}
35 direction: DOWN
36- assertVisible: ${REVIEW_TEXT}
37- takeScreenshot: android-native-review-created
.maestro/native-trail/review-delete.yamladded+25-0
Changes to .maestro/native-trail/review-delete.yaml
@@ -0,0 +1,25 @@
1appId: ${APP_ID}
2name: Delete the persisted review through native confirmation
3---
4- launchApp
5- openLink: wildloop://trail/${TRAIL_ID}
6- extendedWaitUntil:
7 visible: ${TRAIL_NAME}
8 timeout: 30000
9- scrollUntilVisible:
10 element:
11 id: trail-delete-review
12 direction: DOWN
13- tapOn:
14 id: trail-delete-review
15- tapOn: '^Delete review$'
16- extendedWaitUntil:
17 visible: 'Leave a review'
18 timeout: 15000
19- assertNotVisible: ${REVIEW_TEXT}
20- assertNotVisible: ${EDITED_REVIEW_TEXT}
21- assertNotVisible:
22 id: trail-delete-review
23- assertVisible:
24 id: trail-sticky-actions
25- takeScreenshot: android-native-review-deleted
.maestro/native-trail/review-edit.yamladded+41-0
Changes to .maestro/native-trail/review-edit.yaml
@@ -0,0 +1,41 @@
1appId: ${APP_ID}
2name: Reopen and edit the persisted native review
3---
4- launchApp
5- openLink: wildloop://trail/${TRAIL_ID}
6- extendedWaitUntil:
7 visible: ${TRAIL_NAME}
8 timeout: 30000
9- scrollUntilVisible:
10 element:
11 id: trail-write-review
12 direction: DOWN
13- tapOn:
14 id: trail-write-review
15- extendedWaitUntil:
16 visible:
17 id: trail-review-content
18 timeout: 15000
19- tapOn:
20 id: trail-review-content
21- eraseText: 2000
22- inputText: ${EDITED_REVIEW_TEXT}
23- hideKeyboard
24- tapOn:
25 id: trail-review-star-4
26- scrollUntilVisible:
27 element:
28 id: trail-review-submit
29 direction: DOWN
30- tapOn:
31 id: trail-review-submit
32- extendedWaitUntil:
33 visible: 'Edit your review'
34 timeout: 15000
35- scrollUntilVisible:
36 element:
37 text: ${EDITED_REVIEW_TEXT}
38 direction: DOWN
39- assertVisible: ${EDITED_REVIEW_TEXT}
40- assertNotVisible: ${REVIEW_TEXT}
41- takeScreenshot: android-native-review-edited
.maestro/native-trail/sign-in.yamladded+27-0
Changes to .maestro/native-trail/sign-in.yaml
@@ -0,0 +1,27 @@
1appId: ${APP_ID}
2---
3- launchApp:
4 clearState: true
5- extendedWaitUntil:
6 visible:
7 id: native-feed-scroll
8 timeout: 30000
9- openLink: wildloop://login?next=/trail/${TRAIL_ID}
10- extendedWaitUntil:
11 visible:
12 id: login-email
13 timeout: 30000
14- tapOn:
15 id: login-email
16- inputText: ${E2E_EMAIL}
17- tapOn:
18 id: login-password
19- inputText: ${E2E_PASSWORD}
20- hideKeyboard
21- tapOn:
22 id: login-submit
23- extendedWaitUntil:
24 visible: ${TRAIL_NAME}
25 timeout: 30000
26- assertVisible:
27 id: trail-sticky-actions
scripts/run-mobile-e2e.tsmodified+9-18
Changes to scripts/run-mobile-e2e.ts
@@ -98,10 +98,9 @@ export function selectAndroidDeepLinkActivity(output: string, packageName: strin
9898 return null
9999}
100100
101export function selectAndroidHomePackage(output: string): string | null {
102 const component = output.split(/\s+/).find(value => value.includes('/'))
103 const packageName = component?.split('/')[0]
104 return packageName || null
101export function assertAndroidPlatformHealthy(events: string): void {
102 if (/am_(anr|crash)/.test(events))
103 throw new Error('Android platform crash/ANR invalidates native acceptance. Inspect the emulator diagnostics.')
105104}
106105
107106export function selectIosSimulator(payload: SimctlDevices): IosDevice | null {
@@ -348,19 +347,10 @@ function seedTestLocation(platform: MobilePlatform, deviceId: string): void {
348347 ])
349348}
350349
351/**
352 * Google APIs images occasionally leave Pixel Launcher in an ANR state while
353 * Wildloop is foregrounded. The launcher is not part of this journey, so stop
354 * that background process before Maestro launches the app under test.
355 */
356function stopAndroidHomeLauncher(deviceId: string): void {
357 const home = execute([
358 'adb', '-s', deviceId, 'shell', 'cmd', 'package', 'resolve-activity', '--brief',
359 '-a', 'android.intent.action.MAIN', '-c', 'android.intent.category.HOME',
360 ], { capture: true })
361 const packageName = selectAndroidHomePackage(home)
362 if (packageName)
363 execute(['adb', '-s', deviceId, 'shell', 'am', 'force-stop', packageName])
350function checkAndroidPlatform(deviceId: string): void {
351 assertAndroidPlatformHealthy(execute([
352 'adb', '-s', deviceId, 'logcat', '-b', 'events', '-d', '-v', 'threadtime',
353 ], { capture: true }))
364354}
365355
366356function appId(platform: MobilePlatform): string {
@@ -384,9 +374,10 @@ function runAndroid(preview: boolean): void {
384374 console.log(`Wildloop is open on Android device ${devices[0]}.`)
385375 }
386376 else {
387 stopAndroidHomeLauncher(devices[0])
377 checkAndroidPlatform(devices[0])
388378 seedTestLocation('android', devices[0])
389379 runMaestroJourneys('android', devices[0], null)
380 checkAndroidPlatform(devices[0])
390381 }
391382}
392383
scripts/run-native-trail-android-e2e.tsadded+133-0
Changes to scripts/run-native-trail-android-e2e.ts
@@ -0,0 +1,133 @@
1import { mkdir, readFile, writeFile } from 'node:fs/promises'
2import { join, resolve } from 'node:path'
3import process from 'node:process'
4import mobile from '../config/mobile'
5import { QA_PORTS } from '../tests/browser/qa-ports'
6import { QA_SOCIAL_ACCOUNT } from '../tests/browser/qa-social-account'
7import { assertAndroidPlatformHealthy, maestroEnvironment, maestroReportSummary, parseAdbDevices, validateGeneratedNativeApp } from './run-mobile-e2e'
8
9interface PersistedReview { id: number, rating: number, content: string }
10
11/** Check server persistence independently of the native screen's local state. */
12export function assertNativeReviewPersistence(
13 reviews: PersistedReview[], phase: 'create' | 'edit' | 'delete', content: string, previousId?: number,
14): number | undefined {
15 if (phase === 'delete') {
16 if (reviews.length) throw new Error('The native delete did not remove the persisted review.')
17 return
18 }
19 const rating = phase === 'create' ? 5 : 4
20 if (reviews.length !== 1 || reviews[0].content !== content || reviews[0].rating !== rating)
21 throw new Error(`The native ${phase} did not persist the expected review and rating.`)
22 if (phase === 'edit' && reviews[0].id !== previousId)
23 throw new Error('Editing created a second review instead of updating the original.')
24 return reviews[0].id
25}
26
27/** Use only an owned emulator, a disposable database and normally published SDKs. */
28export async function runNativeTrailAndroidE2e(): Promise<void> {
29 const root = resolve('storage/framework/runtime/e2e/native-trail-android')
30 const project = resolve('storage/framework/mobile/android')
31 const origin = `http://127.0.0.1:${QA_PORTS.app}`
32 await mkdir(root, { recursive: true })
33 async function command(args: string[], name: string, env: Record<string, string> = {}): Promise<string> {
34 console.log(`Native Android trail QA: ${name}`)
35 const child = Bun.spawn(args, { env: { ...process.env, ...env }, stdout: 'pipe', stderr: 'pipe' })
36 const [stdout, stderr, status] = await Promise.all([
37 new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited,
38 ])
39 await writeFile(join(root, `${name}.log`), stdout + stderr)
40 if (status) throw new Error(`${args[0]} exited ${status}; see ${name}.log`)
41 return stdout
42 }
43 if (!process.argv.includes('--skip-build')) {
44 await command(['bun', 'run', 'build:android'], 'app-build', { MOBILE_URL: `${origin}/feed` })
45 }
46 validateGeneratedNativeApp(project)
47 const config = JSON.parse(await readFile(join(project, 'craft.config.json'), 'utf8'))
48 if (config.devServerURL !== `${origin}/feed` || config.mapProvider !== 'maplibre')
49 throw new Error('Android trail QA requires its own loopback API and the native MapLibre provider.')
50 const gradle = await readFile(join(project, 'app/build.gradle.kts'), 'utf8')
51 if (!gradle.includes('org.maplibre.gl:android-sdk:') || gradle.includes('play-services-maps'))
52 throw new Error('Android trail QA requires the real key-free native map SDK.')
53 if (process.argv.includes('--build-only')) return
54
55 const devices = parseAdbDevices(await command(['adb', 'devices'], 'devices'))
56 const device = process.env.NATIVE_QA_ANDROID_DEVICE
57 if (!device || !devices.includes(device)) throw new Error('NATIVE_QA_ANDROID_DEVICE must name an owned, ready emulator.')
58 const adb = (args: string[], name: string) => command(['adb', '-s', device, ...args], name)
59 const platformEvents = async (phase: string) => {
60 const events = await adb(['logcat', '-b', 'events', '-d', '-v', 'threadtime'], `platform-${phase}`)
61 assertAndroidPlatformHealthy(events)
62 }
63 await platformEvents('before')
64 for (const port of [QA_PORTS.app, QA_PORTS.api]) {
65 const occupied = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(1000) }).then(() => true).catch(() => false)
66 if (occupied) throw new Error(`QA port ${port} is occupied; select a different QA_PORT_BASE.`)
67 }
68 const qa = Bun.spawn(['bun', 'scripts/start-recording-qa.ts'], { stdout: Bun.file(join(root, 'qa-server.log')), stderr: 'inherit' })
69 const forwards: number[] = []
70 try {
71 const deadline = Date.now() + 180000
72 while (!await fetch(`${origin}/api/health`, { signal: AbortSignal.timeout(1000) }).then(r => r.ok).catch(() => false)) {
73 if (qa.exitCode !== null || Date.now() > deadline) throw new Error('Isolated native QA server did not become ready.')
74 await Bun.sleep(250)
75 }
76 for (const port of [QA_PORTS.app, QA_PORTS.api]) {
77 await adb(['reverse', `tcp:${port}`, `tcp:${port}`], `reverse-${port}`)
78 forwards.push(port)
79 }
80 const listed = await (await fetch(`${origin}/api/trails?q=Craggy&country=all&limit=10`)).json() as any
81 let trail: any
82 for (const candidate of listed.trails ?? []) {
83 const detail = await (await fetch(`${origin}/api/trails/${candidate.id}`)).json() as any
84 if (detail.trail?.hasGeometry) { trail = detail.trail; break }
85 }
86 if (!trail) throw new Error('Native Android QA requires its seeded recorded route.')
87 const reviews = async (): Promise<PersistedReview[]> => {
88 const response = await fetch(`${origin}/api/trails/${trail.id}/reviews`, { cache: 'no-store' })
89 if (!response.ok) throw new Error(`Reading persisted reviews failed: ${response.status}`)
90 return ((await response.json()) as any).reviews
91 }
92 if ((await reviews()).length) throw new Error('The isolated trail must start without reviews.')
93 await adb(['install', '-r', join(project, 'app/build/outputs/apk/debug/app-debug.apk')], 'app-install')
94 const marker = crypto.randomUUID().slice(0, 8)
95 const review = `Android QA ${marker}: clear signs and a beautiful ridge.`
96 const edit = `Android QA ${marker}: updated after a return visit.`
97 const variables = {
98 APP_ID: mobile.android.packageName, TRAIL_ID: String(trail.id), TRAIL_NAME: trail.name,
99 E2E_EMAIL: QA_SOCIAL_ACCOUNT.email, E2E_PASSWORD: QA_SOCIAL_ACCOUNT.sessionSecret,
100 REVIEW_TEXT: review, EDITED_REVIEW_TEXT: edit,
101 }
102 let reviewId: number | undefined
103 for (const phase of ['maps', 'review-create', 'review-edit', 'review-delete'] as const) {
104 const report = join(root, `${phase}.xml`)
105 await command([
106 'maestro', `--device=${device}`, '--no-ansi', 'test',
107 ...Object.entries(variables).flatMap(([key, value]) => ['--env', `${key}=${value}`]),
108 '--format', 'junit', '--output', report, '--debug-output', join(root, 'debug', phase),
109 '--test-output-dir', join(root, 'tests', phase), `.maestro/native-trail/${phase}.yaml`,
110 ], phase, maestroEnvironment())
111 const summary = maestroReportSummary(await readFile(report, 'utf8'))
112 if (!summary.tests || summary.failures) throw new Error(`Native Android ${phase} did not pass.`)
113 if (phase !== 'maps') {
114 const saved = await reviews()
115 await writeFile(join(root, `${phase}-persistence.json`), JSON.stringify(saved, null, 2))
116 const action = phase === 'review-create' ? 'create' : phase === 'review-edit' ? 'edit' : 'delete'
117 reviewId = assertNativeReviewPersistence(saved, action, action === 'create' ? review : edit, reviewId)
118 }
119 await platformEvents(phase)
120 }
121 console.log('Native Android maps, sticky save and persisted review create/edit/delete passed.')
122 }
123 finally {
124 await adb(['logcat', '-d', '-s', 'CraftNativeAndroid:V', 'AndroidRuntime:E'], 'app-runtime').catch(() => {})
125 await adb(['shell', 'uiautomator', 'dump', '/sdcard/wildloop-trail-final.xml'], 'final-tree').then(() =>
126 adb(['pull', '/sdcard/wildloop-trail-final.xml', join(root, 'final-tree.xml')], 'pull-final-tree')).catch(() => {})
127 for (const port of forwards) await adb(['reverse', '--remove', `tcp:${port}`], `remove-reverse-${port}`).catch(() => {})
128 qa.kill('SIGTERM')
129 await qa.exited
130 }
131}
132
133if (import.meta.main) await runNativeTrailAndroidE2e()
tests/unit/mobile-e2e-runner.test.tsmodified+5-4
Changes to tests/unit/mobile-e2e-runner.test.ts
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
22import { tmpdir } from 'node:os'
33import { join } from 'node:path'
44import { describe, expect, it } from 'bun:test'
5import { deepLinkFlow, maestroEnvironment, maestroReportSummary, parseAdbDevices, requestedPlatform, resolveJavaHome, selectAndroidDeepLinkActivity, selectAndroidHomePackage, selectIosSimulator, signedInTargetURL, validateAnalyticsScriptCount, validateBundledFrontend, validateGeneratedNativeApp, validateIosAppBundle, validateNativeShellStyles, validateNativeTabLinks } from '../../scripts/run-mobile-e2e'
5import { assertAndroidPlatformHealthy, deepLinkFlow, maestroEnvironment, maestroReportSummary, parseAdbDevices, requestedPlatform, resolveJavaHome, selectAndroidDeepLinkActivity, selectIosSimulator, signedInTargetURL, validateAnalyticsScriptCount, validateBundledFrontend, validateGeneratedNativeApp, validateIosAppBundle, validateNativeShellStyles, validateNativeTabLinks } from '../../scripts/run-mobile-e2e'
66import { inferDevelopmentTeam, requiredIosAppPaths, requiresDevelopmentTeam, selectAvailableIphone } from '../../scripts/run-ios-device'
77
88function writeNativeNavigationFixtures(output: string, intercepted = ''): void {
@@ -118,9 +118,10 @@ describe('mobile E2E runner', () => {
118118 expect(selectAndroidDeepLinkActivity(output, 'org.missing.app')).toBeNull()
119119 })
120120
121 it('selects the Android home package without assuming a launcher vendor', () => {
122 expect(selectAndroidHomePackage('com.google.android.apps.nexuslauncher/.NexusLauncherActivity\n')).toBe('com.google.android.apps.nexuslauncher')
123 expect(selectAndroidHomePackage('')).toBeNull()
121 it('rejects emulator crashes and ANRs rather than hiding the system launcher', () => {
122 expect(() => assertAndroidPlatformHealthy('am_resume_activity: org.wildloop.app')).not.toThrow()
123 expect(() => assertAndroidPlatformHealthy('am_anr: com.android.launcher3')).toThrow('invalidates native acceptance')
124 expect(() => assertAndroidPlatformHealthy('am_crash: org.wildloop.app')).toThrow('invalidates native acceptance')
124125 })
125126
126127 it('accepts only supported platform arguments', () => {
tests/unit/native-review-persistence.test.tsadded+20-0
Changes to tests/unit/native-review-persistence.test.ts
@@ -0,0 +1,20 @@
1import { describe, expect, it } from 'bun:test'
2import { assertNativeReviewPersistence } from '../../scripts/run-native-trail-android-e2e'
3
4describe('native review acceptance persistence checks', () => {
5 it('requires the edited rating and text on the original review', () => {
6 const created = { id: 7, content: 'First visit.', rating: 5 }
7 const id = assertNativeReviewPersistence([created], 'create', created.content)
8 expect(id).toBe(7)
9 expect(() => assertNativeReviewPersistence([created], 'edit', 'Return visit.', id)).toThrow('expected review')
10 expect(() => assertNativeReviewPersistence([{ ...created, id: 8, content: 'Return visit.', rating: 4 }], 'edit', 'Return visit.', id)).toThrow('second review')
11 expect(assertNativeReviewPersistence([{ ...created, content: 'Return visit.', rating: 4 }], 'edit', 'Return visit.', id)).toBe(id)
12 })
13
14 it('rejects duplicate creates and a review left behind after deletion', () => {
15 const row = { id: 7, content: 'First visit.', rating: 5 }
16 expect(() => assertNativeReviewPersistence([row, { ...row, id: 8 }], 'create', row.content)).toThrow('expected review')
17 expect(() => assertNativeReviewPersistence([row], 'delete', row.content)).toThrow('did not remove')
18 expect(assertNativeReviewPersistence([], 'delete', row.content)).toBeUndefined()
19 })
20})
tsconfig.app.jsonmodified+1-0
Changes to tsconfig.app.json
@@ -79,6 +79,7 @@
7979 "scripts/test-recording-api.ts",
8080 "scripts/start-recording-qa.ts",
8181 "scripts/run-native-trail-e2e.ts",
82 "scripts/run-native-trail-android-e2e.ts",
8283 "tests/native/**/*.ts",
8384 "tests/unit/account-deletion.test.ts",
8485 "tests/unit/battle-rows.test.ts",