feat: add pw tests for onboarding flow

This commit is contained in:
Ramesh Mane
2025-08-21 07:57:25 +00:00
parent 0ce9051636
commit acde2a46e6
5 changed files with 288 additions and 7 deletions

View File

@@ -0,0 +1,214 @@
import { Locator, Page } from '@playwright/test';
import BasePage from '../Base';
import { expect } from '@playwright/test';
export class OnboardingFlowPage extends BasePage {
/**
* Left side image preview section
*/
readonly imagePreviewSection: Locator;
/**
* Right side content section
*/
readonly header: Locator;
/**
* Footer
*/
readonly footer: Locator;
readonly skipButton: Locator;
readonly backButton: Locator;
readonly nextButton: Locator;
/**
* Content
*/
readonly content: Locator;
readonly question: Locator;
/**
* Question options
*/
readonly questionOptions: Locator;
constructor(rootPage: Page) {
super(rootPage);
this.skipButton = this.get().getByTestId('nc-onboarding-flow-skip-button');
this.backButton = this.get().getByTestId('nc-onboarding-flow-back-button');
this.nextButton = this.get().getByTestId('nc-onboarding-flow-next-button');
this.question = this.get().getByTestId('nc-onboarding-flow-question');
this.imagePreviewSection = this.get().getByTestId('nc-onboarding-flow-image-preview-section');
this.header = this.get().getByTestId('nc-onboarding-flow-header');
this.content = this.get().getByTestId('nc-onboarding-flow-content');
this.footer = this.get().getByTestId('nc-onboarding-flow-footer');
// Question options are within the question container
this.questionOptions = this.question.locator('.nc-onboarding-option');
}
get() {
return this.rootPage.getByTestId('nc-onboarding-flow-container');
}
/**
* Check if current question is single select
*/
async isSingleSelectQuestion(): Promise<boolean> {
return (await this.question.evaluate(el => el.classList.contains('nc-single-select-question'))) ?? false;
}
/**
* Check if current question is multi select
*/
async isMultiSelectQuestion(): Promise<boolean> {
return (await this.question.evaluate(el => el.classList.contains('nc-multi-select-question'))) ?? false;
}
/**
* Helper method to get current question type
*/
async getCurrentQuestionType(): Promise<'singleSelect' | 'multiSelect' | void> {
if (await this.isSingleSelectQuestion()) {
return 'singleSelect';
} else if (await this.isMultiSelectQuestion()) {
return 'multiSelect';
}
}
/**
* Check if current question is the first question
*/
async isFirstQuestion(): Promise<boolean> {
return await this.question.evaluate(el => el.classList.contains('nc-first-question'));
}
/**
* Check if current question is the last question
*/
async isLastQuestion(): Promise<boolean> {
return await this.question.evaluate(el => el.classList.contains('nc-last-question'));
}
/**
* Verify current question index
*/
async verifyQuestionIndex(index: number) {
await expect(this.question).toHaveClass(`nc-active-question-index-${index}`);
}
/**
* Test single select question behavior with auto-navigation
*/
async handleSingleSelectQuestion({ optionIndex }: { optionIndex: number }) {
const option = this.questionOptions.nth(optionIndex);
// Select option
await option.click();
await expect(option).toHaveClass(/nc-selected/);
// Wait for auto-navigation (500ms delay as per useOnboardingFlow)
await this.rootPage.waitForTimeout(600);
}
/**
* Test multi select question behavior (no auto-navigation)
*/
async handleMultiSelectQuestion({ optionIndexes }: { optionIndexes: number[] }) {
for (const optionIndex of optionIndexes) {
const option = this.questionOptions.nth(optionIndex);
await option.click();
await expect(option).toHaveClass(/nc-selected/);
}
await this.navigateToNextQuestion();
// Wait for auto-navigation (500ms delay as per useOnboardingFlow)
await this.rootPage.waitForTimeout(600);
}
/**
* Navigate to next question manually (for multi-select or when auto-navigation doesn't work)
*/
async navigateToNextQuestion() {
await this.nextButton.click();
// Wait for transition
await this.rootPage.waitForTimeout(300);
}
/**
* Navigate back to previous question
*/
async navigateToPreviousQuestion() {
await this.backButton.click();
// Wait for transition
await this.rootPage.waitForTimeout(300);
}
/**
* Complete the onboarding flow by navigating through all questions
*/
async completeOnboardingFlow() {
// Navigate through all questions
let questionIndex = 0;
let flag = true;
while (flag) {
await this.verifyQuestionIndex(questionIndex);
const questionType = await this.getCurrentQuestionType();
if (questionType === 'singleSelect') {
await this.handleSingleSelectQuestion({ optionIndex: 0 });
} else if (questionType === 'multiSelect') {
await this.handleMultiSelectQuestion({ optionIndexes: [0, 1] });
}
/**
* `questionIndex > 25` condition will not be reached in the actual flow,
* but it's a safety net to prevent infinite loops in case of unexpected behavior.
*/
if ((await this.isLastQuestion()) || questionIndex > 25) {
flag = false;
break;
}
questionIndex++;
}
// On last question, the next button should say "Finish"
await expect(this.nextButton).toHaveText('Finish');
await this.waitForResponse({
uiAction: () => this.nextButton.click(),
httpMethodsToMatch: ['PATCH'],
requestUrlPathToMatch: 'api/v1/user/profile',
});
// Wait for redirection
await this.rootPage.waitForTimeout(1000);
await expect(this.get()).not.toBeVisible();
}
/**
* Skip the onboarding flow
*/
async skipOnboardingFlow({ verify = false }: { verify?: boolean } = {}) {
await this.waitForResponse({
uiAction: () => this.skipButton.click(),
httpMethodsToMatch: ['PATCH'],
requestUrlPathToMatch: 'api/v1/user/profile',
});
// Wait for redirection
await this.rootPage.waitForTimeout(1000);
if (verify) {
await expect(this.get()).not.toBeVisible();
} else {
await this.get().waitFor({ state: 'hidden' });
}
}
}

View File

@@ -29,11 +29,13 @@ export class SignupPage extends BasePage {
password,
withoutPrefix,
expectedError,
skipOnboardingFlow = true,
}: {
email: string;
password: string;
withoutPrefix?: boolean;
expectedError?: string;
skipOnboardingFlow?: boolean;
}) {
if (!withoutPrefix) email = this.prefixEmail(email);
@@ -47,6 +49,10 @@ export class SignupPage extends BasePage {
await expect(signUp.getByTestId('nc-signup-error')).toHaveText(expectedError);
} else {
await this.rootPage.waitForLoadState('networkidle');
if (skipOnboardingFlow) {
// await this.rootPage.locator('button:has-text("Skip")').click();
}
}
}
}

View File

@@ -0,0 +1,28 @@
import { test } from '@playwright/test';
import { OnboardingFlowPage } from '../pages/OnboardingFlow';
test.describe('Onboarding Flow', () => {
let onboardingFlowPage: OnboardingFlowPage;
test.beforeEach(async ({ page }) => {
// Navigate to the onboarding flow page
await page.goto('/');
// Wait for onboarding flow to be visible
await page.waitForSelector('[data-testid="nc-onboarding-flow-container"]');
onboardingFlowPage = new OnboardingFlowPage(page);
});
test.describe('Flow Completion', () => {
test('should complete onboarding flow', async () => {
// Complete the entire flow
await onboardingFlowPage.completeOnboardingFlow();
});
test('should skip onboarding flow', async () => {
// Skip the flow
await onboardingFlowPage.skipOnboardingFlow();
});
});
});