⋯ 3 lines Show the 3 lines above this hunk @@ -4,6 +4,7 @@ import process from 'node:process' 4 4 import mobile from '../config/mobile' 5 5 import { QA_PORTS } from '../tests/browser/qa-ports' 6 6 import { QA_SOCIAL_ACCOUNT } from '../tests/browser/qa-social-account' 7 + import { decodeRouteParts , primaryRoutePart } from '../resources/functions/trail-geometry' 7 8 import { assertAndroidPlatformHealthy , maestroEnvironment , maestroReportSummary , parseAdbDevices , validateGeneratedNativeApp } from './run-mobile-e2e' 8 9 9 10 interface PersistedReview { id : number , rating : number , content : string } ⋯ 44 lines Show the 44 lines above this hunk @@ -54,7 +55,7 @@ export async function runNativeTrailAndroidE2e(): Promise<void> { 54 55 55 56 const devices = parseAdbDevices ( await command ( [ 'adb' , 'devices' ] , 'devices' ) ) 56 57 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 + if ( ! device || ! / ^ emulator - \ d + $ / . test ( device ) || ! devices . includes ( device ) ) throw new Error ( 'NATIVE_QA_ANDROID_DEVICE must name an owned, ready emulator.' ) 58 59 const adb = ( args : string [ ] , name : string ) => command ( [ 'adb' , '-s' , device , ... args ] , name ) 59 60 const platformEvents = async ( phase : string ) => { 60 61 const events = await adb ( [ 'logcat' , '-b' , 'events' , '-d' , '-v' , 'threadtime' ] , `platform-${phase}` ) ⋯ 23 lines Show the 23 lines above this hunk @@ -84,6 +85,9 @@ export async function runNativeTrailAndroidE2e(): Promise<void> { 84 85 if ( detail . trail ? . hasGeometry ) { trail = detail . trail ; break } 85 86 } 86 87 if ( ! trail ) throw new Error ( 'Native Android QA requires its seeded recorded route.' ) 88 + const first = primaryRoutePart ( [ ... decodeRouteParts ( trail . geometry ) , ... ( Array . isArray ( trail . pieceRoutes ) ? trail . pieceRoutes : [ ] ) ] ) [ 0 ] 89 + if ( ! first ) throw new Error ( 'The seeded trail has no recorded route start.' ) 90 + await adb ( [ 'emu' , 'geo' , 'fix' , String ( first [ 1 ] ) , String ( first [ 0 ] ) ] , 'seed-location' ) 87 91 const reviews = async ( ) : Promise < PersistedReview [ ] > => { 88 92 const response = await fetch ( `${origin}/api/trails/${trail.id}/reviews` , { cache : 'no-store' } ) 89 93 if ( ! response . ok ) throw new Error ( `Reading persisted reviews failed: ${response.status}` ) ⋯ 8 lines Show the 8 lines above this hunk @@ -98,9 +102,10 @@ export async function runNativeTrailAndroidE2e(): Promise<void> { 98 102 APP_ID : mobile . android . packageName , TRAIL_ID : String ( trail . id ) , TRAIL_NAME : trail . name , 99 103 E2E_EMAIL : QA_SOCIAL_ACCOUNT . email , E2E_PASSWORD : QA_SOCIAL_ACCOUNT . sessionSecret , 100 104 REVIEW_TEXT : review , EDITED_REVIEW_TEXT : edit , 105 + GPX_NAME : `${trail.name.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').slice(0, 80)}.gpx` , 101 106 } 102 107 let reviewId : number | undefined 103 - for ( const phase of [ 'maps' , 'review-create' , 'review-edit' , 'review-delete' ] as const ) { 108 + for ( const phase of [ 'maps' , ' utilities' , 'sharing' , 'directions' , ' review-create' , 'review-edit' , 'review-delete' ] as const ) { 104 109 const report = join ( root , `${phase}.xml` ) 105 110 await command ( [ 106 111 'maestro' , `--device=${device}` , '--no-ansi' , 'test' , ⋯ 3 lines Show the 3 lines above this hunk @@ -110,15 +115,31 @@ export async function runNativeTrailAndroidE2e(): Promise<void> { 110 115 ] , phase , maestroEnvironment ( ) ) 111 116 const summary = maestroReportSummary ( await readFile ( report , 'utf8' ) ) 112 117 if ( ! summary . tests || summary . failures ) throw new Error ( `Native Android ${phase} did not pass.` ) 113 - if ( phase !== ' maps ' ) { 118 + if ( phase . startsWith ( ' review- ' ) ) { 114 119 const saved = await reviews ( ) 115 120 await writeFile ( join ( root , `${phase}-persistence.json` ) , JSON . stringify ( saved , null , 2 ) ) 116 121 const action = phase === 'review-create' ? 'create' : phase === 'review-edit' ? 'edit' : 'delete' 117 122 reviewId = assertNativeReviewPersistence ( saved , action , action === 'create' ? review : edit , reviewId ) 118 123 } 124 + if ( phase === 'utilities' ) { 125 + const files = await adb ( [ 'shell' , 'run-as' , mobile . android . packageName , 'find' , '.' , '-type' , 'f' ] , 'export-files' ) 126 + const paths = files . split ( / \ r ? \ n / ) . filter ( path => / ^ [ a - zA - Z0 - 9 _ . / - ] + \ . gpx$ / . test ( path ) ) 127 + let exported = false 128 + for ( const path of paths ) { 129 + const xml = await adb ( [ 'exec-out' , 'run-as' , mobile . android . packageName , 'cat' , path ] , 'exported-gpx' ) 130 + if ( xml . includes ( '<gpx ' ) && xml . includes ( '<trkseg>' ) && xml . includes ( `<trkpt lat="${first[0]}" lon="${first[1]}"` ) ) exported = true 131 + } 132 + if ( ! exported ) throw new Error ( 'The native GPX export did not contain the recorded route coordinates.' ) 133 + } 134 + if ( phase === 'directions' ) { 135 + const activities = await adb ( [ 'shell' , 'dumpsys' , 'activity' , 'activities' ] , 'directions-activities' ) 136 + const resumed = activities . split ( '\n' ) . filter ( line => / ( ? : mResumedActivity | topResumedActivity ) : / . test ( line ) ) 137 + if ( ! resumed . length || resumed . some ( line => line . includes ( `${mobile.android.packageName}/` ) ) ) 138 + throw new Error ( 'Directions did not hand off to an external Android activity.' ) 139 + } 119 140 await platformEvents ( phase ) 120 141 } 121 - console . log ( 'Native Android maps, sticky save and persisted review create/edit/delete passed.' ) 142 + console . log ( 'Native Android maps, sticky actions, location, GPX, route customization, actual share-link paste and persisted review create/edit/delete passed.' ) 122 143 } 123 144 finally { 124 145 await adb ( [ 'logcat' , '-d' , '-s' , 'CraftNativeAndroid:V' , 'AndroidRuntime:E' ] , 'app-runtime' ) . catch ( ( ) => { } )