diff --git a/apps/api/src/e2e-data-seeds.ts b/apps/api/src/e2e-data-seeds.ts index 1bce949a..29d89de4 100644 --- a/apps/api/src/e2e-data-seeds.ts +++ b/apps/api/src/e2e-data-seeds.ts @@ -29,6 +29,70 @@ export const e2eCourses: NiceCourseData[] = [ }, ], }, + { + title: "E2E Testing Quiz", + description: "E2E Testing Quiz Description", + imageUrl: "https://placehold.co/600x400", + type: LESSON_TYPE.quiz.key, + state: STATUS.published.key, + isFree: true, + items: [ + { + itemType: LESSON_ITEM_TYPE.question.key, + questionType: "single_choice", + questionBody: "E2E Testing Question", + state: "published", + questionAnswers: [ + { + optionText: "E2E Testing Answer", + isCorrect: true, + position: 0, + }, + ], + }, + { + itemType: LESSON_ITEM_TYPE.question.key, + questionType: "single_choice", + questionBody: "E2E Testing Question 2", + state: "published", + questionAnswers: [ + { + optionText: "single true", + isCorrect: true, + position: 0, + }, + { + optionText: "single false", + isCorrect: false, + position: 1, + }, + ], + }, + { + itemType: LESSON_ITEM_TYPE.question.key, + questionType: "multiple_choice", + questionBody: "E2E Testing Question 3", + state: "published", + questionAnswers: [ + { + optionText: "multiple true a", + isCorrect: true, + position: 0, + }, + { + optionText: "multiple true b", + isCorrect: false, + position: 1, + }, + { + optionText: "multiple false c", + isCorrect: false, + position: 2, + }, + ], + }, + ], + }, ], }, ]; diff --git a/apps/web/e2e/tests/course.spec.ts b/apps/web/e2e/tests/course.spec.ts new file mode 100644 index 00000000..9468a2ff --- /dev/null +++ b/apps/web/e2e/tests/course.spec.ts @@ -0,0 +1,139 @@ +import { expect, test, type Page } from "@playwright/test"; + +const TEST_COURSE = { + name: "For E2E Testing", + description: "E2E Testing Lesson Description", +} as const; + +const PAYMENT_DATA = { + cardNumber: "4242424242424242", + cvc: "123", + expiryMonth: "10", +} as const; + +const URL_PATTERNS = { + course: + /course\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/, + lesson: + /course\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\/lesson\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/, +} as const; + +const QUIZ_ANSWERS = [ + { name: "E2E Testing Answer", isCorrect: true }, + { name: "single false", isCorrect: false }, + { name: "multiple true a", isCorrect: true }, + { name: "multiple true b", isCorrect: true }, + { name: "multiple false c", isCorrect: false }, +]; + +class CourseActions { + constructor(private readonly page: Page) {} + + async searchCourse(): Promise { + await this.page.getByPlaceholder("Search by title...").fill(TEST_COURSE.name); + await expect(this.page.getByRole("button", { name: "Clear All" })).toBeVisible(); + } + + async openCourse(): Promise { + await this.page.getByRole("link", { name: TEST_COURSE.name }).click(); + await expect(this.page).toHaveURL(URL_PATTERNS.course); + await this.verifyCourseContent(); + } + + private async verifyCourseContent(): Promise { + await expect(this.page.getByText(TEST_COURSE.name)).toBeVisible(); + await expect(this.page.getByText(TEST_COURSE.description)).toBeVisible(); + } +} + +class PaymentActions { + constructor(private readonly page: Page) {} + + async fillPaymentForm(expiryYear: number): Promise { + const stripeFrame = this.page.frameLocator('iframe[title="Secure payment input frame"]'); + await stripeFrame.locator("#Field-numberInput").fill(PAYMENT_DATA.cardNumber); + await stripeFrame + .locator("#Field-expiryInput") + .fill(`${PAYMENT_DATA.expiryMonth}${expiryYear}`); + await stripeFrame.locator("#Field-cvcInput").fill(PAYMENT_DATA.cvc); + await expect(this.page.getByText(/Buy for/)).toBeVisible(); + } + + async completePurchase(): Promise { + await this.page.getByRole("button", { name: /Buy for/ }).click(); + } +} + +class EnrollmentActions { + constructor(private readonly page: Page) {} + + async enrollInCourse(): Promise { + await this.page.getByRole("button", { name: "Enroll" }).click(); + } + + async unenrollFromCourse(): Promise { + const unenrollButton = this.page.getByRole("button", { name: "Unenroll" }); + await unenrollButton.waitFor({ state: "visible", timeout: 10000 }); + await expect(unenrollButton).toBeVisible(); + await unenrollButton.click(); + await expect(this.page.getByRole("button", { name: /Enroll - / })).toBeVisible(); + } +} + +class QuizActions { + constructor(private readonly page: Page) {} + + async solveQuiz(): Promise { + await this.page.getByRole("heading", { name: "E2E Testing Quiz" }).click(); + await expect(this.page).toHaveURL(URL_PATTERNS.lesson); + + for (const answer of QUIZ_ANSWERS) { + await this.page.getByRole("button", { name: answer.name }).click(); + } + } + + async checkAndVerifyResults(): Promise { + await this.page.getByRole("button", { name: "Check answers" }).click(); + await expect(this.page.getByRole("dialog")).toBeVisible(); + await expect(this.page.getByText(/Your Score: 33%/)).toBeVisible(); + } + + async resetQuiz(): Promise { + await this.page.getByRole("button", { name: "Try Again" }).click(); + await this.page.getByRole("button", { name: "Clear progress" }).click(); + } +} + +test.describe.serial("Course E2E", () => { + let courseActions: CourseActions; + let paymentActions: PaymentActions; + let enrollmentActions: EnrollmentActions; + let quizActions: QuizActions; + + test.beforeEach(async ({ page }) => { + await page.goto("/"); + courseActions = new CourseActions(page); + paymentActions = new PaymentActions(page); + enrollmentActions = new EnrollmentActions(page); + quizActions = new QuizActions(page); + await courseActions.searchCourse(); + }); + + test("should find, open and enroll the paid course", async () => { + await courseActions.openCourse(); + await enrollmentActions.enrollInCourse(); + await paymentActions.fillPaymentForm(new Date().getFullYear() + 1); + await paymentActions.completePurchase(); + await enrollmentActions.unenrollFromCourse(); + }); + + test("should solve free quiz lesson", async ({ page }) => { + await courseActions.openCourse(); + + await quizActions.solveQuiz(); + await quizActions.checkAndVerifyResults(); + await quizActions.resetQuiz(); + + await page.goBack(); + }); +}); diff --git a/apps/web/e2e/tests/get-course.spec.ts b/apps/web/e2e/tests/get-course.spec.ts deleted file mode 100644 index 9acb3c4b..00000000 --- a/apps/web/e2e/tests/get-course.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { expect, test } from "@playwright/test"; - -test.describe("course", () => { - test("should find, open and enroll the paid course", async ({ page }) => { - await page.goto("/"); - await page.getByPlaceholder("Search by title...").fill("For E2E Testing"); - await expect(page.getByRole("button", { name: "Clear All" })).toBeVisible(); - - await page.getByRole("link", { name: "For E2E Testing" }).click(); - await expect(page).toHaveURL( - /course\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/, - ); - await expect(page.getByText("For E2E Testing")).toBeVisible(); - await expect(page.getByText("E2E Testing Lesson Description")).toBeVisible(); - - await page.getByRole("button", { name: "Enroll" }).click(); - - const stripeFrame = page.frameLocator('iframe[title="Secure payment input frame"]'); - await stripeFrame.locator("#Field-numberInput").fill("4242424242424242"); - await stripeFrame.locator("#Field-expiryInput").fill(`10${new Date().getFullYear() + 1}`); - await stripeFrame.locator("#Field-cvcInput").fill("123"); - await expect(page.getByText(/Buy for/)).toBeVisible(); - - await page.getByRole("button", { name: /Buy for/ }).click(); - - const unenrollButton = page.getByRole("button", { name: "Unenroll" }); - await unenrollButton.waitFor({ state: "visible", timeout: 10000 }); - await expect(unenrollButton).toBeVisible(); - await unenrollButton.click(); - await expect(page.getByRole("button", { name: /Enroll - / })).toBeVisible(); - }); -});