also looking at this
test(ios): tap a real pushed notification that launches the app, and judge what the page gets
#257
5 files
+308
-10
| @@ -438,6 +438,89 @@ export function deepLinkProblems( | ||
| 438 | 438 | return problems |
| 439 | 439 | } |
| 440 | 440 | |
| 441 | /** | |
| 442 | * Where the pushed notification says to go, as a custom key beside `aps`. | |
| 443 | * | |
| 444 | * The part of a push an app routes on, and the part the tap has to carry | |
| 445 | * intact: WildLoop opens the plant camera off a key like this one. | |
| 446 | */ | |
| 447 | export const NOTIFICATION_ROUTE = '/e2e/notification-tap' | |
| 448 | ||
| 449 | /** | |
| 450 | * The push the harness sends with `simctl push` once the app is killed. | |
| 451 | * | |
| 452 | * The body is the run's nonce, which the UI test finds the banner by, and | |
| 453 | * `craftE2E` carries it again as data, for the page to hand back. | |
| 454 | */ | |
| 455 | export function notificationPayload(run: string): string { | |
| 456 | return `${JSON.stringify({ aps: { alert: { title: 'Craft E2E', body: run } }, craftE2E: run, route: NOTIFICATION_ROUTE })}\n` | |
| 457 | } | |
| 458 | ||
| 459 | /** What a page that subscribed late was handed after a tap launched the app. */ | |
| 460 | export interface NotificationTapReport { | |
| 461 | /** craftNotificationResponse events native code dispatched, counted from the page's first line. */ | |
| 462 | dispatched: number | |
| 463 | /** What notifications.onTap handed a subscriber that arrived after the suite. */ | |
| 464 | taps: Record<string, unknown>[] | |
| 465 | /** `typeof` notifications.onTap. */ | |
| 466 | onTap: string | |
| 467 | /** `typeof` what onTap returned, or null when there was no onTap to call. */ | |
| 468 | unsubscribe: string | null | |
| 469 | } | |
| 470 | ||
| 471 | /** | |
| 472 | * The page's report the UI test printed, read out of xcodebuild's output. | |
| 473 | * | |
| 474 | * `ios-uitests/NotificationTapColdStartTests.swift` writes one | |
| 475 | * `CRAFT-E2E-NOTIFICATION-RESULT CRAFT-E2E-NOTIFICATION {json}` line. None, | |
| 476 | * or one whose JSON does not parse, is null: the test never got that far. | |
| 477 | */ | |
| 478 | export function notificationTapReport(output: string): NotificationTapReport | null { | |
| 479 | for (const line of output.split('\n')) { | |
| 480 | const match = line.match(/CRAFT-E2E-NOTIFICATION-RESULT CRAFT-E2E-NOTIFICATION (\{.*\})\s*$/) | |
| 481 | if (!match) continue | |
| 482 | try { | |
| 483 | const report = JSON.parse(match[1]!) as NotificationTapReport | |
| 484 | if (Array.isArray(report.taps)) return report | |
| 485 | } | |
| 486 | catch {} | |
| 487 | } | |
| 488 | return null | |
| 489 | } | |
| 490 | ||
| 491 | /** | |
| 492 | * Why the notification tap cold start is not a pass, one line per reason. | |
| 493 | * | |
| 494 | * #255 lost a tap in two places, and the report tells them apart: nothing | |
| 495 | * dispatched is the app dropping it before any page existed, dispatched but | |
| 496 | * not handed to onTap is the page flushing it before anyone subscribed. | |
| 497 | */ | |
| 498 | export function notificationTapProblems( | |
| 499 | report: NotificationTapReport | null, | |
| 500 | run: string, | |
| 501 | evidence = 'xcodebuild-notification.log', | |
| 502 | ): string[] { | |
| 503 | if (!report) | |
| 504 | return [`the notification tap cold start never reported; see ${evidence}`] | |
| 505 | if (report.onTap !== 'function') | |
| 506 | return [`notifications.onTap is ${report.onTap}, so a page has no way to be handed a tap`] | |
| 507 | ||
| 508 | const problems: string[] = [] | |
| 509 | if (report.unsubscribe !== 'function') | |
| 510 | problems.push(`notifications.onTap returned ${report.unsubscribe}, expected a function that unsubscribes`) | |
| 511 | if (report.dispatched === 0) { | |
| 512 | problems.push('the app never dispatched the tap that launched it; native code dropped it before the page existed') | |
| 513 | return problems | |
| 514 | } | |
| 515 | ||
| 516 | const ours = report.taps.filter(tap => tap.craftE2E === run) | |
| 517 | if (ours.length !== 1 || report.taps.length !== 1) | |
| 518 | problems.push(`a page that subscribed to notifications.onTap after launch was handed ${JSON.stringify(report.taps)}, expected the tap that launched the app once`) | |
| 519 | else if (ours[0]!.route !== NOTIFICATION_ROUTE) | |
| 520 | problems.push(`the tap reached the page without the push's route: it carried ${JSON.stringify(ours[0])}`) | |
| 521 | return problems | |
| 522 | } | |
| 523 | ||
| 441 | 524 | /** The need the Android share case announces before it opens the menu. */ |
| 442 | 525 | export const DISMISS_SHARE_MENU = 'dismiss-share-menu' |
| 443 | 526 | |