Skip to content

Commit e0510b2

Browse files
committed
Merge remote-tracking branch 'refs/remotes/origin/MPI-152' into MPI-152
2 parents c45f17c + 6ccc9e6 commit e0510b2

12 files changed

Lines changed: 228 additions & 104 deletions

src/components/Instructions.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ const handlePauseToggle = () => {
188188
size={24}
189189
className="text-[#ffffff] cursor-pointer hover:opacity-70 transition-opacity duration-300"
190190
onClick={handleBack}
191+
data-testid="back-button"
191192
/>
192193
</motion.div>
193194

@@ -213,6 +214,7 @@ const handlePauseToggle = () => {
213214
transition={{ duration: 0.5, delay: 0.3 }}
214215
className="mt-8 cursor-pointer"
215216
onClick={toggleSound}
217+
data-testid="sound-toggle"
216218
>
217219
<div className="flex items-center justify-center gap-2 mt-16 text-[#ffffff] hover:text-[#ffffff] transition-colors">
218220
{isSoundEnabled ? (
@@ -238,7 +240,7 @@ const handlePauseToggle = () => {
238240
transition={{ duration: 1 }}
239241
className="flex flex-col justify-between items-center text-center min-h-[90vh] gap-6 px-4 py-24 md:py-8 w-full"
240242
>
241-
<h2 className="text-4xl font-bold -mt-24 md:mt-0">
243+
<h2 className="text-4xl font-bold -mt-24 md:mt-0" data-testid="timer">
242244
{formatTime(timeLeft)}
243245
</h2>
244246

@@ -252,6 +254,7 @@ const handlePauseToggle = () => {
252254
<button
253255
onClick={handlePauseToggle}
254256
className="transition-transform duration-300 cursor-pointer hover:scale-125 hover:opacity-70"
257+
data-testid={isPaused ? "play-button" : "pause-button"}
255258
>
256259
{isPaused ? (
257260
<Play size={32} className="text-[#ffffff]" />
@@ -268,6 +271,7 @@ const handlePauseToggle = () => {
268271
animate={{ opacity: 1, y: 0 }}
269272
transition={{ duration: 1.5, delay: 0.3 }}
270273
className="text-lg md:text-xl text-[#ffffff] text-center max-w-md mx-auto"
274+
data-testid="instruction-text"
271275
>
272276
{t(
273277
`instructions.${exerciseType}.${exercise.instructions[currentInstruction].key}`
@@ -280,6 +284,7 @@ const handlePauseToggle = () => {
280284
transition={{ duration: 0.5, delay: 1 }}
281285
className="mt-8 cursor-pointer"
282286
onClick={toggleSound}
287+
data-testid="sound-toggle"
283288
>
284289
<div className="flex items-center justify-center gap-2 text-[#ffffff] hover:text-gray-900 transition-colors">
285290
{isSoundEnabled ? (

src/components/ToggleButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const ToggleButton = () => {
1616
<button
1717
onClick={handleChangeLanguage}
1818
className="w-16 h-8 sm:w-16 sm:h-8 rounded-full relative bg-[var(--color-button)]"
19+
data-testid="language-toggle"
1920
>
2021
<div
2122
className={`w-6 h-6 sm:w-6 sm:h-6 rounded-full absolute top-1 transition-transform bg-cover bg-center flex justify-end items-center ${

src/components/WelcomePage.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@ const WelcomePage = () => {
2323
{t("title-text")}
2424
<span title={t("Explanation478")} className="cursor-pointer text-center"></span>
2525
<button
26-
id='infoButton'
26+
id='infoButton'
27+
data-testid="info-button"
2728
title={t("Explanation478")}
2829
onClick={toggleInfo}
2930
className="hover:underline flex px-3.5 sm:px-3.5 py-1 sm:py-1 text-[#ffffff] bg-[var(--gradient-1-1)] rounded-full shadow-md hover:opacity-80 text-sm sm:text-base max-w-[5.5rem] items-center justify-center"
3031
>
3132
<p className="text-[--font-global] text-[15px] font-bold">i</p>
3233
</button>
33-
<p id='infoText'
34+
<p id='infoText'
35+
data-testid="info-text"
3436
className={`absolute flex px-3.5 sm:px-3.5 py-1 sm:py-1 text-[--font-global] text-[15px] text-[#ffffff] bg-[var(--gradient-1-1)] rounded-md lg:mr-120 ${
3537
isInfoVisible ? 'visible' : 'hidden'}`}
3638
>{t("Explanation478")}</p>

tests/e2e/BreathingExercise.spec.ts

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import TestData from '../fixtures/testData';
3-
import { waitForBreathingExerciseToStart } from '../fixtures/testHelpers';
3+
import { waitForBreathingExerciseToStart, waitForExerciseTimer } from '../fixtures/testHelpers';
44

55
test.describe('Breathing Exercise', () => {
66
test.beforeEach(async ({ page }) => {
@@ -22,7 +22,7 @@ test.describe('Breathing Exercise', () => {
2222
await expect(page.locator('h2').filter({ hasText: /breathing exercise/i })).toBeVisible();
2323
await expect(page.locator('p').filter({ hasText: /inhale.*for.*4.*seconds.*hold.*for.*7.*seconds.*and.*exhale.*for.*8.*seconds/i })).toBeVisible();
2424

25-
const soundToggle = page.locator('svg[class*="lucide-volume"]');
25+
const soundToggle = page.locator(TestData.selectors.soundToggle);
2626
await expect(soundToggle).toBeVisible();
2727
});
2828

@@ -35,8 +35,7 @@ test.describe('Breathing Exercise', () => {
3535
console.log('H2 elements found:', h2Elements);
3636

3737
// The exercise should show a timer when running
38-
// Look for any h2 that contains time format (MM:SS or M:SS)
39-
const timerElement = page.locator('h2').filter({ hasText: /\d+:\d+/ });
38+
const timerElement = page.locator(TestData.selectors.timer);
4039
const isTimerVisible = await timerElement.isVisible();
4140

4241
if (!isTimerVisible) {
@@ -52,17 +51,17 @@ test.describe('Breathing Exercise', () => {
5251
// If timer is visible, check for controls
5352
await expect(timerElement).toBeVisible();
5453

55-
const pausePlayButton = page.locator('button svg[class*="lucide-pause"], button svg[class*="lucide-play"]');
54+
const pausePlayButton = page.locator(TestData.selectors.pauseButton).or(page.locator(TestData.selectors.playButton));
5655
await expect(pausePlayButton).toBeVisible();
57-
await expect(page.locator('p').filter({ hasText: /through your nose|hold your breath|through your mouth|inhale|exhale/i })).toBeVisible();
56+
await expect(page.locator(TestData.selectors.instructionText)).toBeVisible();
5857
}
5958
});
6059

6160
test('should have back navigation button', async ({ page }) => {
6261
// Wait for the page to load first
6362
await waitForBreathingExerciseToStart(page);
6463

65-
const backButton = page.locator('svg[class*="lucide-arrow-left"]');
64+
const backButton = page.locator(TestData.selectors.backButton);
6665
await expect(backButton).toBeVisible();
6766
});
6867
});
@@ -71,54 +70,58 @@ test.describe('Breathing Exercise', () => {
7170
test('should toggle pause/play when pause button is clicked', async ({ page }) => {
7271
await waitForBreathingExerciseToStart(page);
7372

74-
await page.waitForFunction(
75-
() => {
76-
const h1Elements = Array.from(document.querySelectorAll('h1'));
77-
const timerH2 = Array.from(document.querySelectorAll('h2')).find(
78-
el => /\d+:\d+/.test(el.textContent || '')
79-
);
80-
return h1Elements.length === 0 && timerH2 !== undefined;
81-
},
82-
{ timeout: 15000 }
83-
);
84-
85-
const timerElement = page.locator('h2').filter({ hasText: /\d+:\d+/ });
86-
await expect(timerElement).toBeVisible();
87-
88-
const pauseButton = page.locator('button').filter({ has: page.locator('svg[class*="lucide-pause"]') });
89-
await expect(pauseButton).toBeVisible();
90-
await pauseButton.click();
91-
92-
const playButton = page.locator('button').filter({ has: page.locator('svg[class*="lucide-play"]') });
93-
await expect(playButton).toBeVisible();
94-
await playButton.click();
95-
await expect(pauseButton).toBeVisible();
73+
// Wait for timer to appear (exercise has started, intro phase is over)
74+
// This accounts for the ~13 second intro phase in CI
75+
await waitForExerciseTimer(page, 25000);
76+
77+
// Wait for pause button to be visible
78+
const pauseButton = page.locator(TestData.selectors.pauseButton);
79+
await expect(pauseButton).toBeVisible({ timeout: 15000 });
80+
81+
// Click pause button and wait for play button to appear
82+
// Use force: true because button is constantly animating (never "stable")
83+
await pauseButton.click({ force: true });
84+
85+
// Wait for play button to appear after pause
86+
const playButton = page.locator(TestData.selectors.playButton);
87+
await expect(playButton).toBeVisible({ timeout: 15000 });
88+
89+
// Click play button
90+
// Use force: true because button is constantly animating (never "stable")
91+
await playButton.click({ force: true });
92+
93+
// Wait for pause button to reappear
94+
await expect(pauseButton).toBeVisible({ timeout: 15000 });
9695
});
9796

9897
test('should navigate back when back button is clicked', async ({ page }) => {
9998
await waitForBreathingExerciseToStart(page);
10099

101-
const backButton = page.locator('svg[class*="lucide-arrow-left"]');
100+
const backButton = page.locator(TestData.selectors.backButton);
102101
await backButton.click();
103102
await expect(page).toHaveURL('/');
104103
});
105104

106105
test('should toggle sound when sound button is clicked', async ({ page }) => {
107106
await waitForBreathingExerciseToStart(page);
108107

109-
const soundButton = page.locator('div.mt-8.cursor-pointer');
110-
const initialVolume = await page.locator('svg[class*="lucide-volume2"]').isVisible();
108+
const soundToggle = page.locator(TestData.selectors.soundToggle);
109+
await expect(soundToggle).toBeVisible({ timeout: 15000 });
110+
111+
// Check initial state - look for Volume2 icon (sound enabled) or VolumeX (sound disabled)
112+
const initialSoundEnabled = await page.locator('svg[class*="lucide-volume2"]').isVisible();
111113

112-
await soundButton.click();
114+
await soundToggle.click();
113115

114-
if (initialVolume) {
115-
await expect(page.locator('svg[class*="lucide-volume2"]')).not.toBeVisible();
116+
// Wait for state to change
117+
if (initialSoundEnabled) {
118+
await expect(page.locator('svg[class*="lucide-volume2"]')).not.toBeVisible({ timeout: 5000 });
116119
} else {
117-
await expect(page.locator('svg[class*="lucide-volume2"]')).toBeVisible();
120+
await expect(page.locator('svg[class*="lucide-volume2"]')).toBeVisible({ timeout: 5000 });
118121
}
119122

120-
const newVolume = await page.locator('svg[class*="lucide-volume2"]').isVisible();
121-
expect(newVolume).not.toBe(initialVolume);
123+
const newSoundEnabled = await page.locator('svg[class*="lucide-volume2"]').isVisible();
124+
expect(newSoundEnabled).not.toBe(initialSoundEnabled);
122125
});
123126
});
124127

@@ -127,8 +130,8 @@ test.describe('Breathing Exercise', () => {
127130
await waitForBreathingExerciseToStart(page);
128131

129132
// Check if timer is already visible (exercise started)
130-
const timer = page.locator('h2').filter({ hasText: /\d+:\d+/ });
131-
const isTimerVisible = await timer.isVisible();
133+
const timer = page.locator(TestData.selectors.timer);
134+
const isTimerVisible = await timer.isVisible({ timeout: 5000 }).catch(() => false);
132135

133136
if (!isTimerVisible) {
134137
// Exercise is in intro mode, skip this test
@@ -150,12 +153,12 @@ test.describe('Breathing Exercise', () => {
150153
await expect(introInstructions).toBeVisible();
151154

152155
// Check if exercise is running (has timer)
153-
const timer = page.locator('h2').filter({ hasText: /\d+:\d+/ });
154-
const isTimerVisible = await timer.isVisible();
156+
const timer = page.locator(TestData.selectors.timer);
157+
const isTimerVisible = await timer.isVisible({ timeout: 5000 }).catch(() => false);
155158

156159
if (isTimerVisible) {
157160
// Exercise is running, check for dynamic instructions
158-
const instructions = page.locator('p').filter({ hasText: /inhale|exhale|hold/i });
161+
const instructions = page.locator(TestData.selectors.instructionText);
159162
await expect(instructions).toBeVisible();
160163

161164
const instructionText = await instructions.textContent();
@@ -170,8 +173,8 @@ test.describe('Breathing Exercise', () => {
170173

171174
await page.setViewportSize(TestData.viewports.mobile);
172175
await expect(page.locator('h2').filter({ hasText: /breathing exercise/i })).toBeVisible();
173-
await expect(page.locator('svg[class*="lucide-arrow-left"]')).toBeVisible();
174-
await expect(page.locator('svg[class*="lucide-volume"]')).toBeVisible();
176+
await expect(page.locator(TestData.selectors.backButton)).toBeVisible();
177+
await expect(page.locator(TestData.selectors.soundToggle)).toBeVisible();
175178
});
176179

177180
test('should maintain functionality on tablet', async ({ page }) => {

tests/e2e/HomePage.spec.ts

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,16 @@ test.describe('Homepage', () => {
4545
test('should handle language switching', async ({ page }) => {
4646
await closeQuickEscapeModal(page);
4747

48-
// Visible En
49-
await expectUiLanguage(page, 'EN');
48+
// Visible En
49+
await expectUiLanguage(page, 'EN');
5050

51-
// Change to Es and Verify
52-
await homePage.switchLanguage('ES');
53-
await expectUiLanguage(page, 'ES');
51+
// Change to Es and Verify
52+
await homePage.switchLanguage('ES');
53+
await expectUiLanguage(page, 'ES');
5454

55-
// Change to En and Verify
56-
await homePage.switchLanguage('EN');
57-
await expectUiLanguage(page, 'EN');
55+
// Change to En and Verify
56+
await homePage.switchLanguage('EN');
57+
await expectUiLanguage(page, 'EN');
5858

5959
});
6060

@@ -64,34 +64,42 @@ test.describe('Homepage', () => {
6464
});
6565

6666
test('should navigate to correct donate page in Spanish', async ({ page }) => {
67-
68-
const languageToggle = page.locator('button').filter({ hasText: 'En' });
69-
const DonateButtonEs = page.getByRole ('link').filter({hasText: 'Donar'});
70-
7167
await closeQuickEscapeModal(page);
72-
await languageToggle.click();
73-
await expect(page.locator('text="Donar"')).toBeVisible();
74-
const [newPageEs] = await Promise.all([
75-
page.waitForEvent('popup'),
76-
DonateButtonEs.click(),
77-
]);
78-
await newPageEs.waitForLoadState();
79-
await newPageEs.waitForURL('https://www.thetrevorproject.mx/dona/');
80-
await expect(newPageEs).toHaveURL('https://www.thetrevorproject.mx/dona/');
68+
69+
const languageToggle = page.locator(TestData.selectors.languageToggle);
70+
await languageToggle.waitFor({ state: 'visible', timeout: 10000 });
71+
await languageToggle.click({ timeout: 15000 });
72+
73+
// Wait for language switch to complete - check for Spanish text
74+
await expect(page.locator('text="Donar"')).toBeVisible({ timeout: 10000 });
75+
76+
const DonateButtonEs = page.getByRole('link').filter({ hasText: 'Donar' });
77+
await DonateButtonEs.waitFor({ state: 'visible', timeout: 10000 });
78+
79+
const [newPageEs] = await Promise.all([
80+
page.waitForEvent('popup', { timeout: 15000 }),
81+
DonateButtonEs.click({ timeout: 15000 }),
82+
]);
83+
// Wait for page to load - use domcontentloaded instead of networkidle for external sites
84+
// External sites often have continuous network activity (analytics, ads) so networkidle never fires
85+
await newPageEs.waitForLoadState('domcontentloaded', { timeout: 15000 });
86+
await newPageEs.waitForURL('https://www.thetrevorproject.mx/dona/', { timeout: 15000 });
87+
await expect(newPageEs).toHaveURL('https://www.thetrevorproject.mx/dona/');
8188
});
8289

8390
test('should go to correct donate page in English', async ({ page }) => {
84-
85-
const DonateButtonEn = page.getByRole ('link').filter({hasText: 'Donate'});
86-
8791
await closeQuickEscapeModal(page);
92+
93+
const DonateButtonEn = page.getByRole('link').filter({ hasText: 'Donate' });
8894
await expect(page.locator('text="Donate"')).toBeVisible();
95+
8996
const [newPageEn] = await Promise.all([
90-
page.waitForEvent('popup'),
91-
DonateButtonEn.click(),
97+
page.waitForEvent('popup', { timeout: 15000 }),
98+
DonateButtonEn.click({ timeout: 15000 }),
9299
]);
93-
await newPageEn.waitForLoadState();
94-
await newPageEn.waitForURL('https://give.thetrevorproject.org/campaign/716635/donate');
100+
// Wait for page to load - use domcontentloaded instead of networkidle for external sites
101+
await newPageEn.waitForLoadState('domcontentloaded', { timeout: 15000 });
102+
await newPageEn.waitForURL('https://give.thetrevorproject.org/campaign/716635/donate', { timeout: 15000 });
95103
await expect(newPageEn).toHaveURL('https://give.thetrevorproject.org/campaign/716635/donate');
96104
});
97105

tests/e2e/Navigation.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,10 @@ test.describe('Navigation', () => {
199199
const homePage = new HomePage(page);
200200
await expect(homePage.logo).toBeVisible();
201201

202-
await expect(homePage.logo).toBeEnabled();
202+
// Logo is clickable (has pointer cursor), not a form element so we check visibility instead of enabled
203+
const logoParent = homePage.logo.locator('..');
204+
const cursorStyle = await logoParent.evaluate((el) => window.getComputedStyle(el).cursor).catch(() => '');
205+
expect(cursorStyle).toBe('pointer');
203206
}
204207
});
205208
});

tests/e2e/WelcomePage.spec.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,23 @@ test.describe('WelcomePage', () => {
5757
});
5858

5959
test('should maintain info button functionality across language switches', async () => {
60+
// Wait for info button to be ready
61+
await homePage.infoButton.waitFor({ state: 'visible', timeout: 10000 });
62+
6063
await homePage.clickInfoButton();
61-
await expect(homePage.infoText).toHaveClass(/visible/);
64+
await expect(homePage.infoText).toHaveClass(/visible/, { timeout: 10000 });
6265

6366
await homePage.switchLanguage('ES');
64-
await expect(homePage.infoButton).toBeVisible();
65-
await expect(homePage.infoText).toHaveClass(/visible/);
67+
await expect(homePage.infoButton).toBeVisible({ timeout: 10000 });
68+
await expect(homePage.infoText).toHaveClass(/visible/, { timeout: 10000 });
6669

70+
await homePage.infoButton.waitFor({ state: 'visible', timeout: 10000 });
6771
await homePage.clickInfoButton();
68-
await expect(homePage.infoText).toHaveClass(/hidden/);
72+
await expect(homePage.infoText).toHaveClass(/hidden/, { timeout: 10000 });
6973

74+
await homePage.infoButton.waitFor({ state: 'visible', timeout: 10000 });
7075
await homePage.clickInfoButton();
71-
await expect(homePage.infoText).toHaveClass(/visible/);
76+
await expect(homePage.infoText).toHaveClass(/visible/, { timeout: 10000 });
7277
});
7378
});
7479

tests/fixtures/testData.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,16 @@ export const TestData = {
5656
// Breathing Exercise
5757
breathingCircle: '[data-testid="breathing-circle"]',
5858
pauseButton: '[data-testid="pause-button"]',
59+
playButton: '[data-testid="play-button"]',
5960
resetButton: '[data-testid="reset-button"]',
61+
timer: '[data-testid="timer"]',
62+
soundToggle: '[data-testid="sound-toggle"]',
63+
backButton: '[data-testid="back-button"]',
64+
instructionText: '[data-testid="instruction-text"]',
65+
66+
// Welcome Page
67+
infoButton: '[data-testid="info-button"]',
68+
infoText: '[data-testid="info-text"]',
6069

6170
// Mobile
6271
mobileMenu: '[data-testid="mobile-menu"]',

0 commit comments

Comments
 (0)