test: add QA coverage — access unit tests, VideoPlayer snapshots, Maestro E2E flows, testIDs

- Add testIDs to explore, workout detail, and collection detail screens
- Add testID prop to VideoPlayer component
- Create access service unit tests (isFreeWorkout, canAccessWorkout)
- Create VideoPlayer rendering snapshot tests (preview/background modes)
- Create Maestro E2E flows: explore-freemium, collection-detail
- Update tab-navigation flow with Explore screen assertions
- Update profile-settings flow with real activity stat assertions
- Update all-tests suite to include new flows
This commit is contained in:
Millian Lamiaux
2026-03-24 12:40:02 +01:00
parent a042c348c1
commit 4fa8be600c
12 changed files with 742 additions and 13 deletions

View File

@@ -0,0 +1,82 @@
import { describe, it, expect } from 'vitest'
import React from 'react'
import { render } from '@testing-library/react-native'
import { VideoPlayer } from '@/src/shared/components/VideoPlayer'
describe('VideoPlayer rendering', () => {
describe('preview mode', () => {
it('renders gradient fallback when no videoUrl', () => {
const { toJSON } = render(
<VideoPlayer mode="preview" isPlaying={false} />
)
const tree = toJSON()
expect(tree).toBeTruthy()
expect(tree).toMatchSnapshot()
})
it('renders video view when videoUrl is provided', () => {
const { toJSON } = render(
<VideoPlayer
videoUrl="https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
mode="preview"
isPlaying={true}
/>
)
const tree = toJSON()
expect(tree).toBeTruthy()
expect(tree).toMatchSnapshot()
})
it('renders with custom style', () => {
const { toJSON } = render(
<VideoPlayer
mode="preview"
style={{ height: 220, borderRadius: 20 }}
/>
)
expect(toJSON()).toMatchSnapshot()
})
it('renders with testID prop', () => {
const { getByTestId } = render(
<VideoPlayer
mode="preview"
testID="my-video-player"
/>
)
expect(getByTestId('my-video-player')).toBeTruthy()
})
})
describe('background mode', () => {
it('renders gradient fallback when no videoUrl', () => {
const { toJSON } = render(
<VideoPlayer mode="background" isPlaying={false} />
)
expect(toJSON()).toMatchSnapshot()
})
it('renders video view when videoUrl is provided', () => {
const { toJSON } = render(
<VideoPlayer
videoUrl="https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
mode="background"
isPlaying={true}
/>
)
expect(toJSON()).toMatchSnapshot()
})
})
describe('custom gradient colors', () => {
it('renders with custom gradient colors when no video', () => {
const { toJSON } = render(
<VideoPlayer
gradientColors={['#FF0000', '#0000FF']}
mode="preview"
/>
)
expect(toJSON()).toMatchSnapshot()
})
})
})

View File

@@ -0,0 +1,292 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`VideoPlayer rendering > background mode > renders gradient fallback when no videoUrl 1`] = `
<View
ref={null}
style={
[
{
"backgroundColor": "#000",
"overflow": "hidden",
},
undefined,
]
}
>
<LinearGradient
colors={
[
"#FF6B35",
"#E55A25",
]
}
end={
{
"x": 1,
"y": 1,
}
}
start={
{
"x": 0,
"y": 0,
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
testID="linear-gradient"
/>
</View>
`;
exports[`VideoPlayer rendering > background mode > renders video view when videoUrl is provided 1`] = `
<View
ref={null}
style={
[
{
"backgroundColor": "#000",
"overflow": "hidden",
},
undefined,
]
}
>
<VideoView
contentFit="cover"
nativeControls={false}
player={
{
"currentTime": 0,
"duration": 100,
"muted": false,
"pause": [MockFunction],
"play": [MockFunction] {
"calls": [
[],
],
"results": [
{
"type": "return",
"value": undefined,
},
],
},
"playing": false,
"replace": [MockFunction],
"volume": 1,
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
testID="video-view"
/>
</View>
`;
exports[`VideoPlayer rendering > custom gradient colors > renders with custom gradient colors when no video 1`] = `
<View
ref={null}
style={
[
{
"backgroundColor": "#000",
"overflow": "hidden",
},
undefined,
]
}
>
<LinearGradient
colors={
[
"#FF0000",
"#0000FF",
]
}
end={
{
"x": 1,
"y": 1,
}
}
start={
{
"x": 0,
"y": 0,
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
testID="linear-gradient"
/>
</View>
`;
exports[`VideoPlayer rendering > preview mode > renders gradient fallback when no videoUrl 1`] = `
<View
ref={null}
style={
[
{
"backgroundColor": "#000",
"overflow": "hidden",
},
undefined,
]
}
>
<LinearGradient
colors={
[
"#FF6B35",
"#E55A25",
]
}
end={
{
"x": 1,
"y": 1,
}
}
start={
{
"x": 0,
"y": 0,
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
testID="linear-gradient"
/>
</View>
`;
exports[`VideoPlayer rendering > preview mode > renders video view when videoUrl is provided 1`] = `
<View
ref={null}
style={
[
{
"backgroundColor": "#000",
"overflow": "hidden",
},
undefined,
]
}
>
<VideoView
contentFit="cover"
nativeControls={false}
player={
{
"currentTime": 0,
"duration": 100,
"muted": false,
"pause": [MockFunction],
"play": [MockFunction] {
"calls": [
[],
],
"results": [
{
"type": "return",
"value": undefined,
},
],
},
"playing": false,
"replace": [MockFunction],
"volume": 1,
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
testID="video-view"
/>
</View>
`;
exports[`VideoPlayer rendering > preview mode > renders with custom style 1`] = `
<View
ref={null}
style={
[
{
"backgroundColor": "#000",
"overflow": "hidden",
},
{
"borderRadius": 20,
"height": 220,
},
]
}
>
<LinearGradient
colors={
[
"#FF6B35",
"#E55A25",
]
}
end={
{
"x": 1,
"y": 1,
}
}
start={
{
"x": 0,
"y": 0,
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
testID="linear-gradient"
/>
</View>
`;

View File

@@ -0,0 +1,123 @@
import { describe, it, expect } from 'vitest'
import {
FREE_WORKOUT_IDS,
FREE_WORKOUT_COUNT,
isFreeWorkout,
canAccessWorkout,
} from '../../shared/services/access'
describe('access service', () => {
describe('FREE_WORKOUT_IDS', () => {
it('should contain exactly 3 free workout IDs', () => {
expect(FREE_WORKOUT_IDS).toHaveLength(3)
})
it('should include workout 1 (Full Body Ignite)', () => {
expect(FREE_WORKOUT_IDS).toContain('1')
})
it('should include workout 11 (Core Crusher)', () => {
expect(FREE_WORKOUT_IDS).toContain('11')
})
it('should include workout 43 (Dance Cardio)', () => {
expect(FREE_WORKOUT_IDS).toContain('43')
})
it('should be readonly (immutable)', () => {
// TypeScript enforces readonly at compile time; at runtime we verify the array content is stable
const snapshot = [...FREE_WORKOUT_IDS]
expect(FREE_WORKOUT_IDS).toEqual(snapshot)
})
})
describe('FREE_WORKOUT_COUNT', () => {
it('should equal the length of FREE_WORKOUT_IDS', () => {
expect(FREE_WORKOUT_COUNT).toBe(FREE_WORKOUT_IDS.length)
})
it('should be 3', () => {
expect(FREE_WORKOUT_COUNT).toBe(3)
})
})
describe('isFreeWorkout', () => {
it('should return true for free workout ID "1"', () => {
expect(isFreeWorkout('1')).toBe(true)
})
it('should return true for free workout ID "11"', () => {
expect(isFreeWorkout('11')).toBe(true)
})
it('should return true for free workout ID "43"', () => {
expect(isFreeWorkout('43')).toBe(true)
})
it('should return false for non-free workout ID "2"', () => {
expect(isFreeWorkout('2')).toBe(false)
})
it('should return false for non-free workout ID "10"', () => {
expect(isFreeWorkout('10')).toBe(false)
})
it('should return false for non-free workout ID "44"', () => {
expect(isFreeWorkout('44')).toBe(false)
})
it('should return false for empty string', () => {
expect(isFreeWorkout('')).toBe(false)
})
it('should return false for non-existent ID', () => {
expect(isFreeWorkout('999')).toBe(false)
})
})
describe('canAccessWorkout', () => {
describe('premium user', () => {
it('should access free workout', () => {
expect(canAccessWorkout('1', true)).toBe(true)
})
it('should access non-free workout', () => {
expect(canAccessWorkout('2', true)).toBe(true)
})
it('should access any workout ID', () => {
expect(canAccessWorkout('999', true)).toBe(true)
})
})
describe('free user', () => {
it('should access free workout ID "1"', () => {
expect(canAccessWorkout('1', false)).toBe(true)
})
it('should access free workout ID "11"', () => {
expect(canAccessWorkout('11', false)).toBe(true)
})
it('should access free workout ID "43"', () => {
expect(canAccessWorkout('43', false)).toBe(true)
})
it('should NOT access non-free workout ID "2"', () => {
expect(canAccessWorkout('2', false)).toBe(false)
})
it('should NOT access non-free workout ID "10"', () => {
expect(canAccessWorkout('10', false)).toBe(false)
})
it('should NOT access non-free workout ID "50"', () => {
expect(canAccessWorkout('50', false)).toBe(false)
})
it('should NOT access empty string ID', () => {
expect(canAccessWorkout('', false)).toBe(false)
})
})
})
})

View File

@@ -20,6 +20,8 @@ interface VideoPlayerProps {
/** Whether to play the video */
isPlaying?: boolean
style?: object
/** Test identifier for QA automation */
testID?: string
}
export function VideoPlayer({
@@ -28,6 +30,7 @@ export function VideoPlayer({
mode = 'preview',
isPlaying = true,
style,
testID,
}: VideoPlayerProps) {
const player = useVideoPlayer(videoUrl ?? null, (p) => {
p.loop = true
@@ -47,7 +50,7 @@ export function VideoPlayer({
// No video URL — show gradient fallback
if (!videoUrl) {
return (
<View style={[styles.container, style]}>
<View testID={testID} style={[styles.container, style]}>
<LinearGradient
colors={gradientColors}
start={{ x: 0, y: 0 }}
@@ -59,7 +62,7 @@ export function VideoPlayer({
}
return (
<View style={[styles.container, style]}>
<View testID={testID} style={[styles.container, style]}>
<VideoView
player={player}
style={StyleSheet.absoluteFill}