also looking at this
feat: native trail maps and complete native mobile screens
#1017
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.
| @@ -50,6 +50,13 @@ export async function runNativeTrailE2e(): Promise<void> { | ||
| 50 | 50 | if(detail.trail?.hasGeometry){trail=detail.trail;break} |
| 51 | 51 | } |
| 52 | 52 | 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` | |
| 53 | 60 | const review=`Native QA ${crypto.randomUUID().slice(0,8)}: clear signposts and a beautiful ridge.` |
| 54 | 61 | const edit=' Updated after a return visit.' |
| 55 | 62 | const readReviews=async()=>{ |
| @@ -59,7 +66,7 @@ export async function runNativeTrailE2e(): Promise<void> { | ||
| 59 | 66 | } |
| 60 | 67 | if((await readReviews()).length)throw new Error('The isolated native trail must start without reviews.') |
| 61 | 68 | 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}`}) | |
| 63 | 70 | const controlPort=QA_PORTS.app+15 |
| 64 | 71 | const driver=prepareIosSimulatorDriver({...mobile.ios,simulator:{output:join(root,'driver'),projectName:'WildloopTrailDriver',bundleIdPrefix:'org.wildloop.trailqa',controlUrl:`http://127.0.0.1:${controlPort}`}}) |
| 65 | 72 | 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> { | ||
| 91 | 98 | await command(['xcrun','simctl','location',device,'set',`${first[0]},${first[1]}`],'location-seed.log') |
| 92 | 99 | return new Response('PASS owned simulator location set to the seeded public trailhead') |
| 93 | 100 | } |
| 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 | } | |
| 105 | 101 | if(hook==='verify-gpx') { |
| 106 | 102 | const located=Bun.spawnSync(['xcrun','simctl','get_app_container',device,mobile.ios.bundleId,'data']) |
| 107 | 103 | if(located.exitCode)throw new Error('Could not inspect the owned simulator application container.') |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import type { SimulatorStep } from '@stacksjs/mobile/testing' |
| 2 | 2 | |
| 3 | 3 | /** Real native UI interactions against the isolated QA database. */ |
| 4 | export function nativeTrailJourneys(input:{trailId:number,trailName:string,email:string,password:string,review:string,edit:string}):{name:string,steps:SimulatorStep[],completion:string}[] { | |
| 4 | export 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}[] { | |
| 5 | 5 | const arrival:SimulatorStep[] = [ |
| 6 | 6 | {command:'launch'}, |
| 7 | 7 | {command:'expectid',text:'native-feed-scroll',seconds:15}, |
| @@ -38,6 +38,10 @@ const maps:SimulatorStep[] = [ | ||
| 38 | 38 | {command:'expectexact',text:'Save',seconds:30}, |
| 39 | 39 | {command:'scrollto',text:'Toggle elevation profile'}, |
| 40 | 40 | {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'}, | |
| 41 | 45 | {command:'tapid',text:'trail-profile-toggle'}, |
| 42 | 46 | {command:'expect',text:'Elevation profile collapsed',seconds:10}, |
| 43 | 47 | {command:'scrollto',text:'Toggle elevation profile'}, |
| @@ -64,6 +68,10 @@ const maps:SimulatorStep[] = [ | ||
| 64 | 68 | {command:'expectid',text:'trail-full-map',seconds:10}, |
| 65 | 69 | {command:'shot',name:'native-floating-profile-fullscreen'}, |
| 66 | 70 | {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'}, | |
| 67 | 75 | {command:'tapid',text:'map-zoom-in'}, |
| 68 | 76 | {command:'tapid',text:'map-zoom-out'}, |
| 69 | 77 | {command:'tapid',text:'map-fit'}, |
| @@ -149,7 +157,28 @@ const sharing:SimulatorStep[] = [ | ||
| 149 | 157 | {command:'shot',name:'native-share-sheet'}, |
| 150 | 158 | {command:'tree',name:'native-share-sheet'}, |
| 151 | 159 | {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 | ] | |
| 171 | const 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 | ] | |
| 177 | const 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'}, | |
| 153 | 182 | ] |
| 154 | 183 | const gpx:SimulatorStep[] = [ |
| 155 | 184 | ...fullMap, |
| @@ -210,7 +239,9 @@ return [ | ||
| 210 | 239 | {name:'maps',steps:maps,completion:'PASS expectid trail-sticky-actions'}, |
| 211 | 240 | {name:'reviews',steps:reviews,completion:'exec verify-review-edited: PASS edited review retained its identity and persisted'}, |
| 212 | 241 | {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'}, | |
| 214 | 245 | {name:'gpx',steps:gpx,completion:'exec verify-gpx: PASS exported GPX contains the recorded route'}, |
| 215 | 246 | {name:'planner',steps:planner,completion:'PASS expectid trail-full-map'}, |
| 216 | 247 | {name:'location',steps:location,completion:'PASS expect Your location is shown'}, |