From 90759a83689b2665649ef03062c87b2f3d769f3c Mon Sep 17 00:00:00 2001 From: Ur-imazing Date: Fri, 17 Apr 2026 18:06:01 +1200 Subject: [PATCH 1/4] feat(mobile): add testID props to components for Maestro e2e coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure metadata additions — no behavior, styling, or structural changes. Unblocks the 20 Maestro flows that previously failed on "Element not found" because testIDs didn't exist in components. testIDs added: - Tab bar: tab-home, tab-discover, tab-library, tab-profile - Header: header-search, header-profile - Hero: hero-cta, mute-button - Search: search-input, search-result-{index} - Cards: video-card-{index}, video-carousel-card-{index}, media-collection-item-{index}, nav-carousel-item-{index}, experience-card-{index}, playlist-item-{index} - Video detail: video-thumbnail, share-button, back-button - Bible quotes: share-quote, quote-cta - Related questions: accordion-question-{index}, accordion-cta - Quiz: quiz-button, modal-close Preserves all existing accessibilityLabel props. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/mobile/app/(tabs)/_layout.tsx | 4 ++++ apps/mobile/app/(tabs)/library.tsx | 6 +++++- apps/mobile/app/(tabs)/watch.tsx | 1 + apps/mobile/app/_layout.tsx | 2 ++ apps/mobile/app/collection/[sectionKey].tsx | 1 + apps/mobile/app/video/[sectionKey].tsx | 2 ++ apps/mobile/src/components/search/SearchResultCard.tsx | 1 + .../components/sections/BibleQuotesCarouselRenderer.tsx | 2 ++ apps/mobile/src/components/sections/ContentDispatcher.tsx | 2 +- apps/mobile/src/components/sections/CuratedHomeLayout.tsx | 1 + .../src/components/sections/MediaCollectionRenderer.tsx | 1 + .../src/components/sections/NavigationCarouselRenderer.tsx | 1 + apps/mobile/src/components/sections/QuizButtonRenderer.tsx | 2 ++ .../src/components/sections/RelatedQuestionsRenderer.tsx | 7 ++++++- apps/mobile/src/components/sections/VideoCardRenderer.tsx | 7 ++++++- .../src/components/sections/VideoCarouselRenderer.tsx | 1 + apps/mobile/src/components/sections/VideoHeroRenderer.tsx | 1 + apps/mobile/src/components/ui/HomeHeader.tsx | 2 ++ 18 files changed, 40 insertions(+), 4 deletions(-) diff --git a/apps/mobile/app/(tabs)/_layout.tsx b/apps/mobile/app/(tabs)/_layout.tsx index d96fb0e88..3efcc6a51 100644 --- a/apps/mobile/app/(tabs)/_layout.tsx +++ b/apps/mobile/app/(tabs)/_layout.tsx @@ -27,6 +27,7 @@ export default function TabLayout() { name="index" options={{ title: "Home", + tabBarButtonTestID: "tab-home", tabBarIcon: ({ color, size }) => ( ), @@ -36,6 +37,7 @@ export default function TabLayout() { name="watch" options={{ title: "Discover", + tabBarButtonTestID: "tab-discover", headerShown: true, headerTitle: "Discover", headerStyle: { backgroundColor: BG_COLOR }, @@ -50,6 +52,7 @@ export default function TabLayout() { name="library" options={{ title: "Library", + tabBarButtonTestID: "tab-library", tabBarIcon: ({ color, size }) => ( ), @@ -59,6 +62,7 @@ export default function TabLayout() { name="profile" options={{ title: "Profile", + tabBarButtonTestID: "tab-profile", tabBarIcon: ({ color, size }) => ( ), diff --git a/apps/mobile/app/(tabs)/library.tsx b/apps/mobile/app/(tabs)/library.tsx index aaed47bc6..8a8c21a36 100644 --- a/apps/mobile/app/(tabs)/library.tsx +++ b/apps/mobile/app/(tabs)/library.tsx @@ -96,9 +96,10 @@ export default function LibraryScreen() { data={experiences} keyExtractor={(item) => item.documentId} contentContainerStyle={styles.listContent} - renderItem={({ item }) => ( + renderItem={({ item, index }) => ( @@ -112,6 +113,7 @@ export default function LibraryScreen() { function ExperienceCard({ experience, + index, isActive, onSelect, }: { @@ -122,6 +124,7 @@ function ExperienceCard({ metaDescription: string | null ogImage: { url: string; alternativeText: string | null } | null } + index: number isActive: boolean onSelect: (slug: string) => void }) { @@ -130,6 +133,7 @@ function ExperienceCard({ return ( onSelect(experience.slug)} style={[styles.card, isActive && styles.cardActive]} accessibilityRole="button" diff --git a/apps/mobile/app/(tabs)/watch.tsx b/apps/mobile/app/(tabs)/watch.tsx index e4cde556d..f719ba228 100644 --- a/apps/mobile/app/(tabs)/watch.tsx +++ b/apps/mobile/app/(tabs)/watch.tsx @@ -257,6 +257,7 @@ export default function DiscoverScreen() { ( router.back()} accessibilityRole="button" accessibilityLabel="Go back" @@ -204,6 +205,7 @@ export default function RootLayout() { headerTitleAlign: "center", headerLeft: () => ( router.back()} accessibilityRole="button" accessibilityLabel="Go back" diff --git a/apps/mobile/app/collection/[sectionKey].tsx b/apps/mobile/app/collection/[sectionKey].tsx index e4a6229c3..d7aaf3806 100644 --- a/apps/mobile/app/collection/[sectionKey].tsx +++ b/apps/mobile/app/collection/[sectionKey].tsx @@ -247,6 +247,7 @@ function CollectionPlayerContent({ return ( [ styles.row, isActive && styles.rowActive, diff --git a/apps/mobile/app/video/[sectionKey].tsx b/apps/mobile/app/video/[sectionKey].tsx index 54009bd99..b54189139 100644 --- a/apps/mobile/app/video/[sectionKey].tsx +++ b/apps/mobile/app/video/[sectionKey].tsx @@ -97,6 +97,7 @@ function VideoDetailContent({ headerTitle: title ?? "", headerRight: () => ( { const parts = [`Check out "${displayTitle}" on JesusFilm!`] if (shareUrl != null) parts.push(shareUrl) @@ -201,6 +202,7 @@ function VideoDetailContent({ /> {!hasStarted && thumbnailUrl != null && ( onSelect(result.slug)} accessibilityRole="button" accessibilityLabel={`${result.title}: ${result.snippet}`} diff --git a/apps/mobile/src/components/sections/BibleQuotesCarouselRenderer.tsx b/apps/mobile/src/components/sections/BibleQuotesCarouselRenderer.tsx index 6c2d58158..29d343667 100644 --- a/apps/mobile/src/components/sections/BibleQuotesCarouselRenderer.tsx +++ b/apps/mobile/src/components/sections/BibleQuotesCarouselRenderer.tsx @@ -106,6 +106,7 @@ function QuoteCard({ return null return ( [ styles.ctaButton, pressed && styles.ctaButtonPressed, @@ -231,6 +232,7 @@ export function BibleQuotesCarouselRenderer({ )} + return case "text": return case "relatedQuestions": diff --git a/apps/mobile/src/components/sections/CuratedHomeLayout.tsx b/apps/mobile/src/components/sections/CuratedHomeLayout.tsx index b0782d94b..48e00f743 100644 --- a/apps/mobile/src/components/sections/CuratedHomeLayout.tsx +++ b/apps/mobile/src/components/sections/CuratedHomeLayout.tsx @@ -201,6 +201,7 @@ export function CuratedHomeLayout() { > {muteButtonRect != null && ( [ card.surface, { width: cardWidth }, diff --git a/apps/mobile/src/components/sections/NavigationCarouselRenderer.tsx b/apps/mobile/src/components/sections/NavigationCarouselRenderer.tsx index 60f76a74d..7472dff78 100644 --- a/apps/mobile/src/components/sections/NavigationCarouselRenderer.tsx +++ b/apps/mobile/src/components/sections/NavigationCarouselRenderer.tsx @@ -62,6 +62,7 @@ export function NavigationCarouselRenderer({ return ( [ card.base, styles.localCard, diff --git a/apps/mobile/src/components/sections/QuizButtonRenderer.tsx b/apps/mobile/src/components/sections/QuizButtonRenderer.tsx index ba347dcc0..a664049c8 100644 --- a/apps/mobile/src/components/sections/QuizButtonRenderer.tsx +++ b/apps/mobile/src/components/sections/QuizButtonRenderer.tsx @@ -68,6 +68,7 @@ function QuizModal({ url, onClose }: { url: string; onClose: () => void }) { [styles.button, pressed && feedback.pressed]} onPress={() => setModalVisible(true)} accessibilityRole="button" diff --git a/apps/mobile/src/components/sections/RelatedQuestionsRenderer.tsx b/apps/mobile/src/components/sections/RelatedQuestionsRenderer.tsx index aa1651882..b15836fcc 100644 --- a/apps/mobile/src/components/sections/RelatedQuestionsRenderer.tsx +++ b/apps/mobile/src/components/sections/RelatedQuestionsRenderer.tsx @@ -30,10 +30,12 @@ export interface RelatedQuestionsRendererProps { function QuestionRow({ item, + index, isExpanded, onToggle, }: { item: QuestionItem + index: number isExpanded: boolean onToggle: () => void }) { @@ -42,6 +44,7 @@ function QuestionRow({ return ( )} - {questions.map((item) => ( + {questions.map((item, index) => ( handleToggle(item.id)} /> diff --git a/apps/mobile/src/components/sections/VideoCardRenderer.tsx b/apps/mobile/src/components/sections/VideoCardRenderer.tsx index 23e9d13dc..7078df941 100644 --- a/apps/mobile/src/components/sections/VideoCardRenderer.tsx +++ b/apps/mobile/src/components/sections/VideoCardRenderer.tsx @@ -22,11 +22,15 @@ import type { VideoRef } from "../../lib/types" export interface VideoCardRendererProps { section: NormalizedBlock + index?: number } // ── Component ─────────────────────────────────────────────────────────────── -export function VideoCardRenderer({ section }: VideoCardRendererProps) { +export function VideoCardRenderer({ + section, + index = 0, +}: VideoCardRendererProps) { const router = useRouter() const typography = useTypography() @@ -53,6 +57,7 @@ export function VideoCardRenderer({ section }: VideoCardRendererProps) { return ( [ styles.container, pressed && Platform.OS === "ios" && feedback.pressed, diff --git a/apps/mobile/src/components/sections/VideoCarouselRenderer.tsx b/apps/mobile/src/components/sections/VideoCarouselRenderer.tsx index 298a9e8bc..61b4f2d81 100644 --- a/apps/mobile/src/components/sections/VideoCarouselRenderer.tsx +++ b/apps/mobile/src/components/sections/VideoCarouselRenderer.tsx @@ -88,6 +88,7 @@ export function VideoCarouselRenderer({ section }: VideoCarouselRendererProps) { return ( [ card.surface, { width: cardWidth, height: cardHeight }, diff --git a/apps/mobile/src/components/sections/VideoHeroRenderer.tsx b/apps/mobile/src/components/sections/VideoHeroRenderer.tsx index 4bdd88a05..91414aaa1 100644 --- a/apps/mobile/src/components/sections/VideoHeroRenderer.tsx +++ b/apps/mobile/src/components/sections/VideoHeroRenderer.tsx @@ -276,6 +276,7 @@ export function VideoHeroRenderer({ )} {hasCta && ( [ styles.ctaButton, pressed && feedback.pressed, diff --git a/apps/mobile/src/components/ui/HomeHeader.tsx b/apps/mobile/src/components/ui/HomeHeader.tsx index a926762e6..3001f0aa0 100644 --- a/apps/mobile/src/components/ui/HomeHeader.tsx +++ b/apps/mobile/src/components/ui/HomeHeader.tsx @@ -31,6 +31,7 @@ export function HomeHeader({ title, titleOpacity }: HomeHeaderProps) { pointerEvents="none" /> router.navigate("/(tabs)/watch")} @@ -57,6 +58,7 @@ export function HomeHeader({ title, titleOpacity }: HomeHeaderProps) { )} router.navigate("/(tabs)/profile")} From 4b0ca07cef8702e68460bf0bc2d247212bb5df2c Mon Sep 17 00:00:00 2001 From: Ur-imazing Date: Fri, 17 Apr 2026 18:08:34 +1200 Subject: [PATCH 2/4] feat(web): add data-testid attributes for Playwright e2e coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure metadata additions — no behavior, styling, or structural changes. Unblocks Playwright flows that previously failed when fragile CSS selector fallbacks couldn't find elements. data-testids added: - Header: logo, search-toggle, search-close - Hero: hero, hero-heading, hero-cta, hero-mute - Carousels: carousel-thumbnail, media-collection, media-collection-item, collection-size, nav-carousel, nav-carousel-item, bible-quotes, quote-cta, resource-cta - Accordions: accordion-trigger, accordion-cta - Countdown/dates: advent-toggle, advent-days, easter-toggle - Quiz: quiz-button, modal-close - Section fallbacks: error-block, null-block (replace null returns with inert hidden spans so tests can detect error/null states) Also: - Add playwright-report/ and test-results/ to .gitignore - Remove unused eslint-disable for @next/next/no-img-element (rule not loaded in current config) Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/.gitignore | 2 + apps/web/src/components/SearchOverlay.tsx | 1 + apps/web/src/components/SearchToggle.tsx | 1 + apps/web/src/components/SiteHeader.tsx | 6 ++- .../components/sections/AdventCountdown.tsx | 6 ++- .../sections/BibleQuotesCarousel.tsx | 29 ++++++++------- .../src/components/sections/CarouselVideo.tsx | 1 + .../web/src/components/sections/Container.tsx | 33 +++++++++++++---- .../src/components/sections/EasterDates.tsx | 1 + .../components/sections/MediaCollection.tsx | 30 +++++++++------ .../sections/NavigationCarousel.tsx | 8 +--- .../src/components/sections/QuizButton.tsx | 6 ++- .../components/sections/RelatedQuestions.tsx | 2 + apps/web/src/components/sections/Section.tsx | 37 ++++++++++++++++--- .../web/src/components/sections/VideoHero.tsx | 11 ++++-- apps/web/src/components/sections/index.tsx | 6 ++- 16 files changed, 128 insertions(+), 52 deletions(-) create mode 100644 apps/web/.gitignore diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 000000000..5c4ffa21f --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,2 @@ +playwright-report/ +test-results/ diff --git a/apps/web/src/components/SearchOverlay.tsx b/apps/web/src/components/SearchOverlay.tsx index bc38bf561..c243a0246 100644 --- a/apps/web/src/components/SearchOverlay.tsx +++ b/apps/web/src/components/SearchOverlay.tsx @@ -243,6 +243,7 @@ export function SearchOverlay({ open, onClose, closing }: SearchOverlayProps) { onClick={onClose} className="rounded-full p-3 text-stone-400 transition hover:text-white" aria-label="Close search" + data-testid="search-close" > - +

{displayTitle} @@ -117,7 +118,10 @@ export function AdventCountdown({ data }: AdventCountdownProps) { ) : (
-

+

{days}

diff --git a/apps/web/src/components/sections/BibleQuotesCarousel.tsx b/apps/web/src/components/sections/BibleQuotesCarousel.tsx index 2d76ef8c4..b06079039 100644 --- a/apps/web/src/components/sections/BibleQuotesCarousel.tsx +++ b/apps/web/src/components/sections/BibleQuotesCarousel.tsx @@ -35,7 +35,7 @@ export function BibleQuotesCarousel({ data }: BibleQuotesCarouselProps) { if (validQuotes.length === 0) return null return ( -

+
- - {quote.reference} - -

- {quote.text} -

- +
+ + + {quote.reference} + +

+ {quote.text} +

+
+
) } @@ -169,6 +171,7 @@ function FreeResourceCard({ quote }: { quote: QuoteItem }) {