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.

scripts/run-native-trail-e2e.tsmodified+8-12
Changes to scripts/run-native-trail-e2e.ts
@@ -50,6 +50,13 @@ export async function runNativeTrailE2e(): Promise<void> {
5050 if(detail.trail?.hasGeometry){trail=detail.trail;break}
5151 }
5252 if(!trail)throw new Error('Native trail QA requires its seeded recorded route.')
53 const profileResponse=await fetch(`${origin}/api/trails/${trail.id}/profile`)
54 if(!profileResponse.ok)throw new Error('Native iOS QA requires the trail profile endpoint to be available.')
55 const profile=await profileResponse.json() as {points:Array<{mi:number,ft:number|null}>}
56 const endpoint=profile.points?.at(-1)
57 if(!endpoint || profile.points.length<2 || !Number.isFinite(endpoint.mi) || (endpoint.ft!==null && !Number.isFinite(endpoint.ft)))
58 throw new Error('Native iOS QA requires a selectable recorded route profile.')
59 const endElevation=endpoint.ft===null ? 'elevation unavailable' : `${endpoint.ft} ft`
5360 const review=`Native QA ${crypto.randomUUID().slice(0,8)}: clear signposts and a beautiful ridge.`
5461 const edit=' Updated after a return visit.'
5562 const readReviews=async()=>{
@@ -59,7 +66,7 @@ export async function runNativeTrailE2e(): Promise<void> {
5966 }
6067 if((await readReviews()).length)throw new Error('The isolated native trail must start without reviews.')
6168 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})
69 const journeys=nativeTrailJourneys({trailId:trail.id,trailName:trail.name,email:QA_SOCIAL_ACCOUNT.email,password:QA_SOCIAL_ACCOUNT.sessionSecret,review,edit,profileEndText:`${endpoint.mi.toFixed(2)} mi · ${endElevation}`,inlineProfileEndText:`${endpoint.mi.toFixed(2)} mi, ${endElevation}`})
6370 const controlPort=QA_PORTS.app+15
6471 const driver=prepareIosSimulatorDriver({...mobile.ios,simulator:{output:join(root,'driver'),projectName:'WildloopTrailDriver',bundleIdPrefix:'org.wildloop.trailqa',controlUrl:`http://127.0.0.1:${controlPort}`}})
6572 control=Bun.serve({hostname:'127.0.0.1',port:controlPort,idleTimeout:120,async fetch(request){
@@ -91,17 +98,6 @@ export async function runNativeTrailE2e(): Promise<void> {
9198 await command(['xcrun','simctl','location',device,'set',`${first[0]},${first[1]}`],'location-seed.log')
9299 return new Response('PASS owned simulator location set to the seeded public trailhead')
93100 }
94 if(hook==='verify-share') {
95 console.log('Native trail QA: inspect the owned simulator share clipboard')
96 const pasted=Bun.spawnSync(['xcrun','simctl','pbpaste',device],{timeout:15000,killSignal:'SIGKILL',maxBuffer:1048576})
97 await writeFile(join(root,'sharing-clipboard.log'),JSON.stringify({exitCode:pasted.exitCode,signalCode:pasted.signalCode,stdout:pasted.stdout.toString(),stderr:pasted.stderr.toString()},null,2))
98 if(pasted.exitCode!==0 || !pasted.stdout.toString().includes(`https://wildloop.org/trail/${trail.id}`)) {
99 const verbose=Bun.spawnSync(['xcrun','simctl','pbpaste','-v',device],{timeout:15000,killSignal:'SIGKILL',maxBuffer:1048576})
100 await writeFile(join(root,'sharing-clipboard-types.log'),JSON.stringify({exitCode:verbose.exitCode,signalCode:verbose.signalCode,stdout:verbose.stdout.toString(),stderr:verbose.stderr.toString()},null,2))
101 throw new Error('The native share action did not copy this trail link. Inspect sharing-clipboard.log and sharing-clipboard-types.log.')
102 }
103 return new Response('PASS native share copied the trail link')
104 }
105101 if(hook==='verify-gpx') {
106102 const located=Bun.spawnSync(['xcrun','simctl','get_app_container',device,mobile.ios.bundleId,'data'])
107103 if(located.exitCode)throw new Error('Could not inspect the owned simulator application container.')
tests/native/trail-journey.tsmodified+34-3
Changes to tests/native/trail-journey.ts
@@ -1,7 +1,7 @@
11import type { SimulatorStep } from '@stacksjs/mobile/testing'
22
33/** Real native UI interactions against the isolated QA database. */
4export function nativeTrailJourneys(input:{trailId:number,trailName:string,email:string,password:string,review:string,edit:string}):{name:string,steps:SimulatorStep[],completion:string}[] {
4export function nativeTrailJourneys(input:{trailId:number,trailName:string,email:string,password:string,review:string,edit:string,profileEndText:string,inlineProfileEndText:string}):{name:string,steps:SimulatorStep[],completion:string}[] {
55const arrival:SimulatorStep[] = [
66 {command:'launch'},
77 {command:'expectid',text:'native-feed-scroll',seconds:15},
@@ -38,6 +38,10 @@ const maps:SimulatorStep[] = [
3838 {command:'expectexact',text:'Save',seconds:30},
3939 {command:'scrollto',text:'Toggle elevation profile'},
4040 {command:'shot',name:'native-floating-profile-inline'},
41 {command:'sliderid',id:'trail-elevation-slider',position:1},
42 {command:'expectexact',text:input.inlineProfileEndText,seconds:10},
43 {command:'shot',name:'native-inline-elevation-endpoint'},
44 {command:'tree',name:'native-inline-elevation-endpoint'},
4145 {command:'tapid',text:'trail-profile-toggle'},
4246 {command:'expect',text:'Elevation profile collapsed',seconds:10},
4347 {command:'scrollto',text:'Toggle elevation profile'},
@@ -64,6 +68,10 @@ const maps:SimulatorStep[] = [
6468 {command:'expectid',text:'trail-full-map',seconds:10},
6569 {command:'shot',name:'native-floating-profile-fullscreen'},
6670 {command:'tree',name:'native-floating-profile-fullscreen'},
71 {command:'sliderid',id:'map-elevation-slider',position:1},
72 {command:'expectexact',text:input.profileEndText,seconds:10},
73 {command:'shot',name:'native-elevation-endpoint'},
74 {command:'tree',name:'native-elevation-endpoint'},
6775 {command:'tapid',text:'map-zoom-in'},
6876 {command:'tapid',text:'map-zoom-out'},
6977 {command:'tapid',text:'map-fit'},
@@ -149,7 +157,28 @@ const sharing:SimulatorStep[] = [
149157 {command:'shot',name:'native-share-sheet'},
150158 {command:'tree',name:'native-share-sheet'},
151159 {command:'tapexact',text:'Copy'},
152 {command:'exec',name:'verify-share'},
160 {command:'openurl',url:'wildloop://login'},
161 {command:'expectid',text:'login-email',seconds:10},
162 {command:'valueid',id:'login-email',text:''},
163 {command:'longpressid',text:'login-email'},
164 {command:'tapexact',text:'Paste'},
165 {command:'valueid',id:'login-email',text:`https://wildloop.org/trail/${input.trailId}`},
166 {command:'shot',name:'native-shared-link-pasted'},
167 {command:'tree',name:'native-shared-link-pasted'},
168 {command:'openurl',url:`wildloop://trail/${input.trailId}`},
169 {command:'expectid',text:'trail-sticky-actions',seconds:15},
170]
171const directions:SimulatorStep[] = [
172 ...arrival,
173 {command:'tapid',text:'trail-directions'},
174 {command:'expectapp',text:'com.apple.Maps',seconds:30},
175 {command:'systemtree',name:'native-sticky-directions-handoff'},
176]
177const mapDirections:SimulatorStep[] = [
178 ...fullMap,
179 {command:'tapid',text:'map-directions'},
180 {command:'expectapp',text:'com.apple.Maps',seconds:30},
181 {command:'systemtree',name:'native-map-directions-handoff'},
153182]
154183const gpx:SimulatorStep[] = [
155184 ...fullMap,
@@ -210,7 +239,9 @@ return [
210239 {name:'maps',steps:maps,completion:'PASS expectid trail-sticky-actions'},
211240 {name:'reviews',steps:reviews,completion:'exec verify-review-edited: PASS edited review retained its identity and persisted'},
212241 {name:'review-delete',steps:reviewDelete,completion:'exec verify-review-deleted: PASS deleted review is absent from persistence'},
213 {name:'sharing',steps:sharing,completion:'exec verify-share: PASS native share copied the trail link'},
242 {name:'sharing',steps:sharing,completion:'PASS expectid trail-sticky-actions'},
243 {name:'directions',steps:directions,completion:'PASS expectapp com.apple.Maps'},
244 {name:'map-directions',steps:mapDirections,completion:'PASS expectapp com.apple.Maps'},
214245 {name:'gpx',steps:gpx,completion:'exec verify-gpx: PASS exported GPX contains the recorded route'},
215246 {name:'planner',steps:planner,completion:'PASS expectid trail-full-map'},
216247 {name:'location',steps:location,completion:'PASS expect Your location is shown'},