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+1-0
Changes to .github/workflows/mobile-e2e.yml
@@ -174,6 +174,7 @@ jobs:
174174 path: |
175175 storage/framework/runtime/e2e/native-trail/*.log
176176 storage/framework/runtime/e2e/native-trail/*.txt
177 storage/framework/runtime/e2e/native-trail/*-persistence.json
177178 storage/framework/runtime/e2e/native-trail/*.png
178179 storage/framework/runtime/e2e/native-trail/results-*.xcresult
179180 if-no-files-found: warn
scripts/run-native-trail-e2e.tsmodified+26-1
Changes to scripts/run-native-trail-e2e.ts
@@ -51,7 +51,15 @@ export async function runNativeTrailE2e(): Promise<void> {
5151 }
5252 if(!trail)throw new Error('Native trail QA requires its seeded recorded route.')
5353 const review=`Native QA ${crypto.randomUUID().slice(0,8)}: clear signposts and a beautiful ridge.`
54 const journeys=nativeTrailJourneys({trailId:trail.id,trailName:trail.name,email:QA_SOCIAL_ACCOUNT.email,password:QA_SOCIAL_ACCOUNT.sessionSecret,review,edit:' Updated after a return visit.'})
54 const edit=' Updated after a return visit.'
55 const readReviews=async()=>{
56 const response=await fetch(`${origin}/api/trails/${trail.id}/reviews`,{cache:'no-store'})
57 if(!response.ok)throw new Error(`Reading persisted reviews failed: ${response.status}`)
58 return ((await response.json()) as any).reviews as Array<{id:number,content:string,rating:number}>
59 }
60 if((await readReviews()).length)throw new Error('The isolated native trail must start without reviews.')
61 let createdReviewId:number|undefined
62 const journeys=nativeTrailJourneys({trailId:trail.id,trailName:trail.name,email:QA_SOCIAL_ACCOUNT.email,password:QA_SOCIAL_ACCOUNT.sessionSecret,review,edit})
5563 const controlPort=QA_PORTS.app+15
5664 const driver=prepareIosSimulatorDriver({...mobile.ios,simulator:{output:join(root,'driver'),projectName:'WildloopTrailDriver',bundleIdPrefix:'org.wildloop.trailqa',controlUrl:`http://127.0.0.1:${controlPort}`}})
5765 control=Bun.serve({hostname:'127.0.0.1',port:controlPort,idleTimeout:120,async fetch(request){
@@ -60,6 +68,23 @@ export async function runNativeTrailE2e(): Promise<void> {
6068 if(url.pathname==='/exec') {
6169 try {
6270 const hook=url.searchParams.get('name')
71 if(hook==='verify-review-created' || hook==='verify-review-edited' || hook==='verify-review-deleted') {
72 const rows=await readReviews()
73 await writeFile(join(root,`${hook}-persistence.json`),JSON.stringify(rows,null,2))
74 if(hook==='verify-review-deleted') {
75 if(rows.length)throw new Error('The native deletion did not remove the persisted review.')
76 return new Response('PASS deleted review is absent from persistence')
77 }
78 if(rows.length!==1)throw new Error('The native review action must persist exactly one review.')
79 if(hook==='verify-review-created') {
80 if(rows[0].content!==review || rows[0].rating!==5)throw new Error('The created review text and rating did not persist.')
81 createdReviewId=rows[0].id
82 return new Response('PASS created review text and rating persisted')
83 }
84 if(rows[0].id!==createdReviewId || rows[0].rating!==4 || !rows[0].content.includes(edit.trim()))
85 throw new Error('The native edit did not retain the review identity and persist its new text and rating.')
86 return new Response('PASS edited review retained its identity and persisted')
87 }
6388 if(hook==='seed-location') {
6489 const first=primaryRoutePart([...decodeRouteParts(trail.geometry),...(Array.isArray(trail.pieceRoutes)?trail.pieceRoutes:[])])[0]
6590 if(!first)throw new Error('The seeded trail has no recorded route start.')
tests/native/trail-journey.tsmodified+8-3
Changes to tests/native/trail-journey.ts
@@ -79,27 +79,32 @@ const reviews:SimulatorStep[] = [
7979 {command:'scrollto',text:'Edit your review'},
8080 {command:'shot',name:'native-review-created'},
8181 {command:'tree',name:'native-review-created'},
82 {command:'exec',name:'verify-review-created'},
8283 {command:'tapid',text:'trail-write-review'},
8384 {command:'expectid',text:'trail-review-composer',seconds:10},
85 {command:'tapid',text:'trail-review-star-4'},
8486 {command:'typeid',id:'trail-review-content',text:input.edit},
8587 {command:'scrollto',text:'Save review'},
8688 {command:'tapid',text:'trail-review-submit'},
8789 {command:'expect',text:input.edit.trim(),seconds:15},
8890 {command:'shot',name:'native-review-edited'},
91 {command:'exec',name:'verify-review-edited'},
8992]
9093// Keep deletion observable even when editing fails earlier in its own phase.
9194// This removes the real review created above; the aggregate still fails if
9295// any part of the lifecycle fails.
9396const reviewDelete:SimulatorStep[] = [
9497 ...arrival,
95 {command:'expect',text:input.review,seconds:15},
98 {command:'expect',text:'Edit your review',seconds:15},
9699 {command:'scrollto',text:'Delete your review'},
97100 {command:'tapid',text:'trail-delete-review'},
98101 {command:'tapexact',text:'Delete review'},
99102 {command:'expect',text:'Leave a review',seconds:15},
100103 {command:'absent',text:input.review},
104 {command:'absent',text:input.edit.trim()},
101105 {command:'shot',name:'native-review-deleted'},
102106 {command:'tree',name:'native-review-deleted'},
107 {command:'exec',name:'verify-review-deleted'},
103108]
104109const fullMap:SimulatorStep[] = [
105110 ...arrival,
@@ -146,8 +151,8 @@ const location:SimulatorStep[] = [
146151]
147152return [
148153 {name:'maps',steps:maps,completion:'PASS expectid trail-sticky-actions'},
149 {name:'reviews',steps:reviews,completion:`PASS expect ${input.edit.trim()}`},
150 {name:'review-delete',steps:reviewDelete,completion:`PASS absent ${input.review}`},
154 {name:'reviews',steps:reviews,completion:'exec verify-review-edited: PASS edited review retained its identity and persisted'},
155 {name:'review-delete',steps:reviewDelete,completion:'exec verify-review-deleted: PASS deleted review is absent from persistence'},
151156 {name:'sharing',steps:sharing,completion:'exec verify-share: PASS native share copied the trail link'},
152157 {name:'gpx',steps:gpx,completion:'exec verify-gpx: PASS exported GPX contains the recorded route'},
153158 {name:'planner',steps:planner,completion:'PASS expectid trail-full-map'},