mirror of
https://github.com/nocodb/nocodb.git
synced 2026-04-25 06:15:46 +00:00
feat: add pw tests for onboarding flow
This commit is contained in:
214
tests/playwright/pages/OnboardingFlow/index.ts
Normal file
214
tests/playwright/pages/OnboardingFlow/index.ts
Normal 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' });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
tests/playwright/tests/onboarding-flow.spec.ts
Normal file
28
tests/playwright/tests/onboarding-flow.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user