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.
| @@ -174,6 +174,7 @@ jobs: | ||
| 174 | 174 | path: | |
| 175 | 175 | storage/framework/runtime/e2e/native-trail/*.log |
| 176 | 176 | storage/framework/runtime/e2e/native-trail/*.txt |
| 177 | storage/framework/runtime/e2e/native-trail/*-persistence.json | |
| 177 | 178 | storage/framework/runtime/e2e/native-trail/*.png |
| 178 | 179 | storage/framework/runtime/e2e/native-trail/results-*.xcresult |
| 179 | 180 | if-no-files-found: warn |
| @@ -51,7 +51,15 @@ export async function runNativeTrailE2e(): Promise<void> { | ||
| 51 | 51 | } |
| 52 | 52 | if(!trail)throw new Error('Native trail QA requires its seeded recorded route.') |
| 53 | 53 | 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}) | |
| 55 | 63 | const controlPort=QA_PORTS.app+15 |
| 56 | 64 | const driver=prepareIosSimulatorDriver({...mobile.ios,simulator:{output:join(root,'driver'),projectName:'WildloopTrailDriver',bundleIdPrefix:'org.wildloop.trailqa',controlUrl:`http://127.0.0.1:${controlPort}`}}) |
| 57 | 65 | 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> { | ||
| 60 | 68 | if(url.pathname==='/exec') { |
| 61 | 69 | try { |
| 62 | 70 | 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 | } | |
| 63 | 88 | if(hook==='seed-location') { |
| 64 | 89 | const first=primaryRoutePart([...decodeRouteParts(trail.geometry),...(Array.isArray(trail.pieceRoutes)?trail.pieceRoutes:[])])[0] |
| 65 | 90 | if(!first)throw new Error('The seeded trail has no recorded route start.') |
| @@ -79,27 +79,32 @@ const reviews:SimulatorStep[] = [ | ||
| 79 | 79 | {command:'scrollto',text:'Edit your review'}, |
| 80 | 80 | {command:'shot',name:'native-review-created'}, |
| 81 | 81 | {command:'tree',name:'native-review-created'}, |
| 82 | {command:'exec',name:'verify-review-created'}, | |
| 82 | 83 | {command:'tapid',text:'trail-write-review'}, |
| 83 | 84 | {command:'expectid',text:'trail-review-composer',seconds:10}, |
| 85 | {command:'tapid',text:'trail-review-star-4'}, | |
| 84 | 86 | {command:'typeid',id:'trail-review-content',text:input.edit}, |
| 85 | 87 | {command:'scrollto',text:'Save review'}, |
| 86 | 88 | {command:'tapid',text:'trail-review-submit'}, |
| 87 | 89 | {command:'expect',text:input.edit.trim(),seconds:15}, |
| 88 | 90 | {command:'shot',name:'native-review-edited'}, |
| 91 | {command:'exec',name:'verify-review-edited'}, | |
| 89 | 92 | ] |
| 90 | 93 | // Keep deletion observable even when editing fails earlier in its own phase. |
| 91 | 94 | // This removes the real review created above; the aggregate still fails if |
| 92 | 95 | // any part of the lifecycle fails. |
| 93 | 96 | const reviewDelete:SimulatorStep[] = [ |
| 94 | 97 | ...arrival, |
| 95 | {command:'expect',text:input.review,seconds:15}, | |
| 98 | {command:'expect',text:'Edit your review',seconds:15}, | |
| 96 | 99 | {command:'scrollto',text:'Delete your review'}, |
| 97 | 100 | {command:'tapid',text:'trail-delete-review'}, |
| 98 | 101 | {command:'tapexact',text:'Delete review'}, |
| 99 | 102 | {command:'expect',text:'Leave a review',seconds:15}, |
| 100 | 103 | {command:'absent',text:input.review}, |
| 104 | {command:'absent',text:input.edit.trim()}, | |
| 101 | 105 | {command:'shot',name:'native-review-deleted'}, |
| 102 | 106 | {command:'tree',name:'native-review-deleted'}, |
| 107 | {command:'exec',name:'verify-review-deleted'}, | |
| 103 | 108 | ] |
| 104 | 109 | const fullMap:SimulatorStep[] = [ |
| 105 | 110 | ...arrival, |
| @@ -146,8 +151,8 @@ const location:SimulatorStep[] = [ | ||
| 146 | 151 | ] |
| 147 | 152 | return [ |
| 148 | 153 | {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'}, | |
| 151 | 156 | {name:'sharing',steps:sharing,completion:'exec verify-share: PASS native share copied the trail link'}, |
| 152 | 157 | {name:'gpx',steps:gpx,completion:'exec verify-gpx: PASS exported GPX contains the recorded route'}, |
| 153 | 158 | {name:'planner',steps:planner,completion:'PASS expectid trail-full-map'}, |