From 0e07bec5e5d0b16ec84a1ab8db1185f6148bfa2b Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Thu, 17 Oct 2024 01:57:00 +0200 Subject: [PATCH] feat: add get paid course e2e test --- .github/workflows/nighty-playwright.yml | 6 +- apps/api/src/e2e-data-seeds.ts | 86 +++++++++++++++++++++++ apps/api/src/nice-data-seeds.ts | 52 +------------- apps/api/src/seed-helpers.ts | 14 ++-- apps/api/src/seed.ts | 6 +- apps/api/src/utils/types/test-types.ts | 49 +++++++++++++ apps/web/.gitignore | 1 + apps/web/e2e/tests/get-course.spec.ts | 56 +++++++++++++++ apps/web/e2e/{ => tests}/login.spec.ts | 0 apps/web/e2e/{ => tests}/register.spec.ts | 2 + apps/web/playwright.config.ts | 11 ++- 11 files changed, 220 insertions(+), 63 deletions(-) create mode 100644 apps/api/src/e2e-data-seeds.ts create mode 100644 apps/api/src/utils/types/test-types.ts create mode 100644 apps/web/e2e/tests/get-course.spec.ts rename apps/web/e2e/{ => tests}/login.spec.ts (100%) rename apps/web/e2e/{ => tests}/register.spec.ts (90%) diff --git a/.github/workflows/nighty-playwright.yml b/.github/workflows/nighty-playwright.yml index f28eb955..f535e4c3 100644 --- a/.github/workflows/nighty-playwright.yml +++ b/.github/workflows/nighty-playwright.yml @@ -1,13 +1,11 @@ -# uncomment when staging will be ready - # name: Nightly Playwright Tests # env: -# HUSKY: 0 +# HUSKY: 0 # on: # schedule: -# - cron: "0 1 * * *" +# - cron: "0 6 * * *" # workflow_dispatch: # jobs: diff --git a/apps/api/src/e2e-data-seeds.ts b/apps/api/src/e2e-data-seeds.ts new file mode 100644 index 00000000..5d581a52 --- /dev/null +++ b/apps/api/src/e2e-data-seeds.ts @@ -0,0 +1,86 @@ +import { faker } from "@faker-js/faker"; +import { CourseData } from "./utils/types/test-types"; + +export const e2eCourses: CourseData[] = [ + { + title: "For E2E Testing", + description: "DO NOT DELETE THIS COURSE", + state: "published", + priceInCents: 9900, + category: "E2E Testing", + imageUrl: faker.image.urlPicsumPhotos(), + lessons: [ + { + title: "E2E Testing Lesson", + description: "E2E Testing Lesson Description", + state: "published", + items: [ + { + type: "text_block", + title: "E2E Testing Text Block", + body: "E2E Testing Text Block Body", + state: "published", + }, + { + type: "question", + questionType: "open_answer", + questionBody: "E2E Testing Question Body", + state: "published", + }, + ], + }, + { + title: "E2E Testing Lesson 2", + description: "E2E Testing Lesson 2 Description", + state: "published", + items: [ + { + type: "text_block", + title: "E2E Testing Text Block 2", + body: "E2E Testing Text Block 2 Body", + state: "published", + }, + { + type: "file", + title: "E2E Testing File", + fileType: "external_video", + url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", + state: "published", + }, + { + type: "question", + questionType: "single_choice", + questionBody: "E2E Testing Single Choice Question Body", + state: "published", + }, + ], + }, + { + title: "E2E Testing Lesson 3", + description: "E2E Testing Lesson 3 Description", + state: "published", + items: [ + { + type: "text_block", + title: "E2E Testing Text Block 3", + body: "E2E Testing Text Block 3 Body", + state: "published", + }, + { + type: "file", + title: "E2E Testing File 2", + fileType: "external_presentation", + url: "https://res.cloudinary.com/dinpapxzv/raw/upload/v1727104719/presentation_gp0o3d.pptx", + state: "published", + }, + { + type: "question", + questionType: "multiple_choice", + questionBody: "E2E Testing Multiple Choice Question Body", + state: "published", + }, + ], + }, + ], + }, +]; diff --git a/apps/api/src/nice-data-seeds.ts b/apps/api/src/nice-data-seeds.ts index 0af98872..3c216ad8 100644 --- a/apps/api/src/nice-data-seeds.ts +++ b/apps/api/src/nice-data-seeds.ts @@ -1,55 +1,7 @@ import { faker } from "@faker-js/faker"; -import { Status } from "./storage/schema/utils"; +import { CourseData } from "./utils/types/test-types"; -export const LessonFileType = { - presentation: "Presentation", - external_presentation: "External Presentation", - video: "Video", - external_video: "External Video", -} as const; - -export const QuestionType = { - open_answer: "Open Answer", - single_choice: "Single Choice", - multiple_choice: "Multiple Choice", -} as const; - -export interface NiceCourseData { - title: string; - description: string; - imageUrl?: string; - state: keyof typeof Status; - priceInCents: number; - category: string; - lessons: { - title: string; - description: string; - state: keyof typeof Status; - items: Array< - | { - type: "text_block"; - title: string; - body: string; - state: keyof typeof Status; - } - | { - type: "file"; - title: string; - fileType: keyof typeof LessonFileType; - url: string; - state: keyof typeof Status; - } - | { - type: "question"; - questionType: keyof typeof QuestionType; - questionBody: string; - state: keyof typeof Status; - } - >; - }[]; -} - -export const niceCourses: NiceCourseData[] = [ +export const niceCourses: CourseData[] = [ { title: "Foundations of Accounting: Principles and Practices", description: diff --git a/apps/api/src/seed-helpers.ts b/apps/api/src/seed-helpers.ts index 946cdeb7..17f588c5 100644 --- a/apps/api/src/seed-helpers.ts +++ b/apps/api/src/seed-helpers.ts @@ -1,4 +1,5 @@ import { faker } from "@faker-js/faker"; +import { sql } from "drizzle-orm/sql"; import { categories, courseLessons, @@ -11,11 +12,14 @@ import { textBlocks, } from "src/storage/schema"; import { DatabasePg } from "./common"; -import { niceCourses } from "./nice-data-seeds"; -import { sql } from "drizzle-orm/sql"; - -export async function createNiceCourses(adminUserId: string, db: DatabasePg) { - for (const courseData of niceCourses) { +import { CourseData } from "./utils/types/test-types"; + +export async function createNiceCourses( + adminUserId: string, + db: DatabasePg, + data: CourseData[], +) { + for (const courseData of data) { const [category] = await db .insert(categories) .values({ diff --git a/apps/api/src/seed.ts b/apps/api/src/seed.ts index cd102ac7..0d02975e 100644 --- a/apps/api/src/seed.ts +++ b/apps/api/src/seed.ts @@ -20,6 +20,8 @@ import { import { createNiceCourses, seedTruncateAllTables } from "./seed-helpers"; import { DatabasePg } from "./common"; import hashPassword from "./common/helpers/hashPassword"; +import { niceCourses } from "./nice-data-seeds"; +import { e2eCourses } from "./e2e-data-seeds"; dotenv.config({ path: "./.env" }); @@ -177,8 +179,10 @@ async function seed() { await createUsers(5); console.log("Created users with credentials"); - await createNiceCourses(adminUser.id, db); + await createNiceCourses(adminUser.id, db, niceCourses); console.log("✨✨✨Created created nice courses✨✨✨"); + await createNiceCourses(adminUser.id, db, e2eCourses); + console.log("🧪 Created e2e courses"); const createdCategories = await createEntities(categories, 6, () => ({ id: faker.string.uuid(), diff --git a/apps/api/src/utils/types/test-types.ts b/apps/api/src/utils/types/test-types.ts new file mode 100644 index 00000000..970b75ed --- /dev/null +++ b/apps/api/src/utils/types/test-types.ts @@ -0,0 +1,49 @@ +import { Status } from "src/storage/schema/utils"; + +export const LessonFileType = { + presentation: "Presentation", + external_presentation: "External Presentation", + video: "Video", + external_video: "External Video", +} as const; + +export const QuestionType = { + open_answer: "Open Answer", + single_choice: "Single Choice", + multiple_choice: "Multiple Choice", +} as const; + +export interface CourseData { + title: string; + description: string; + imageUrl?: string; + state: keyof typeof Status; + priceInCents: number; + category: string; + lessons: { + title: string; + description: string; + state: keyof typeof Status; + items: Array< + | { + type: "text_block"; + title: string; + body: string; + state: keyof typeof Status; + } + | { + type: "file"; + title: string; + fileType: keyof typeof LessonFileType; + url: string; + state: keyof typeof Status; + } + | { + type: "question"; + questionType: keyof typeof QuestionType; + questionBody: string; + state: keyof typeof Status; + } + >; + }[]; +} diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 8b74ac3b..2fd4c930 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -4,3 +4,4 @@ node_modules /build .env /test-results +**/e2e/.auth diff --git a/apps/web/e2e/tests/get-course.spec.ts b/apps/web/e2e/tests/get-course.spec.ts new file mode 100644 index 00000000..6a0d6573 --- /dev/null +++ b/apps/web/e2e/tests/get-course.spec.ts @@ -0,0 +1,56 @@ +import { expect, test } from "@playwright/test"; + +test.describe("course", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/auth/login"); + }); + + test("should find, open and enroll the paid course", async ({ page }) => { + await page.getByLabel("email").fill("user@example.com"); + await page.getByLabel("password").fill("studentpassword"); + await page.getByRole("button", { name: /login/i }).click(); + + await expect(page).toHaveURL("/"); + await expect(page).toHaveTitle(/dashboard/i); + + await page + .getByPlaceholder("Search by name or keyword...") + .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 expect( + page.getByText("E2E Testing Lesson 2 Description") + ).toBeVisible(); + await expect( + page.getByText("E2E Testing Lesson 3 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(); + }); +}); diff --git a/apps/web/e2e/login.spec.ts b/apps/web/e2e/tests/login.spec.ts similarity index 100% rename from apps/web/e2e/login.spec.ts rename to apps/web/e2e/tests/login.spec.ts diff --git a/apps/web/e2e/register.spec.ts b/apps/web/e2e/tests/register.spec.ts similarity index 90% rename from apps/web/e2e/register.spec.ts rename to apps/web/e2e/tests/register.spec.ts index c7eaf37d..d6f1ff9f 100644 --- a/apps/web/e2e/register.spec.ts +++ b/apps/web/e2e/tests/register.spec.ts @@ -6,6 +6,8 @@ test.describe("register page", () => { }); test("register user", async ({ page }) => { + await page.getByLabel("first name").fill("testname"); + await page.getByLabel("last name").fill("testlastname"); await page.getByLabel("email").fill("test@useraaaa.com"); await page.getByLabel("password").fill("password"); diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index 7957c8ec..20d10dda 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, use: { baseURL: process.env.CI - ? "https://staging-url.example.com" + ? "https://lms.beta.selleo.app" : "https://app.lms.localhost", ignoreHTTPSErrors: true, // launchOptions: { @@ -24,13 +24,18 @@ export default defineConfig({ }, projects: [ + { name: "setup", testMatch: /.*\.setup\.ts/ }, { name: "chromium", - use: { ...devices["Desktop Chrome"] }, + use: { + ...devices["Desktop Chrome"], + }, }, { name: "firefox", - use: { ...devices["Desktop Firefox"] }, + use: { + ...devices["Desktop Firefox"], + }, }, ], });