diff --git a/apps/scandic-web/.gitignore b/apps/scandic-web/.gitignore index e6a6e831a..53ebb6542 100644 --- a/apps/scandic-web/.gitignore +++ b/apps/scandic-web/.gitignore @@ -61,4 +61,7 @@ variables.css .swc -public/_static/shared \ No newline at end of file +public/_static/shared + +test-results +playwright-report \ No newline at end of file diff --git a/apps/scandic-web/package.json b/apps/scandic-web/package.json index 4ef3d0718..8fd117ada 100644 --- a/apps/scandic-web/package.json +++ b/apps/scandic-web/package.json @@ -10,7 +10,9 @@ "lint:fix": "next typegen && eslint --fix --max-warnings 0 . && tsgo --noEmit", "start": "node .next/standalone/server.js", "test:setup": "yarn build && yarn start", - "preinstall": "/bin/sh -c \"export $(cat .env.local | grep -v '^#' | xargs)\"", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:debug": "playwright test --debug", "test": "vitest run", "test:watch": "vitest", "ci:build": "yarn lint && yarn test && yarn build", @@ -91,6 +93,7 @@ "@eslint/js": "^9.26.0", "@formatjs/cli": "^6.7.1", "@lokalise/node-api": "^14.0.0", + "@playwright/test": "^1.57.0", "@react-aria/test-utils": "1.0.0-alpha.8", "@scandic-hotels/typescript-config": "workspace:*", "@testing-library/react": "^16.3.0", diff --git a/apps/scandic-web/playwright.config.ts b/apps/scandic-web/playwright.config.ts new file mode 100644 index 000000000..0a5b3e6a7 --- /dev/null +++ b/apps/scandic-web/playwright.config.ts @@ -0,0 +1,95 @@ +import { defineConfig, devices } from "@playwright/test" + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + timeout: 120_000, + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3000", + + /* How long to wait for actions to complete. */ + actionTimeout: 60_000, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: process.env.CI ? "on-first-retry" : "retain-on-failure", + }, + expect: { + timeout: 60_000, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 1400, height: 720 }, + }, + }, + + { + name: "firefox", + use: { + ...devices["Desktop Firefox"], + viewport: { width: 1400, height: 720 }, + }, + }, + + { + name: "webkit", + use: { + ...devices["Desktop Safari"], + viewport: { width: 1400, height: 720 }, + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "yarn dev", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/apps/scandic-web/tests/date.test.tsx b/apps/scandic-web/tests/date.test.tsx deleted file mode 100644 index 6b75d26b0..000000000 --- a/apps/scandic-web/tests/date.test.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* eslint-disable formatjs/no-literal-string-in-jsx */ - -import { User } from "@react-aria/test-utils" -import { userEvent } from "@testing-library/user-event" -import { FormProvider, useForm } from "react-hook-form" -import { afterEach, describe, expect, test } from "vitest" - -import { Lang } from "@scandic-hotels/common/constants/language" -import { dt } from "@scandic-hotels/common/dt" -import { getLocalizedMonthName } from "@scandic-hotels/common/utils/dateFormatting" -import Date from "@scandic-hotels/design-system/Form/Date" - -import { cleanup, render, screen } from "@/tests/utils" - -const testUtilUser = new User({ interactionType: "touch" }) - -interface FormWrapperProps { - defaultValues: Record - children: React.ReactNode - onSubmit: (event: unknown) => void -} - -function FormWrapper({ defaultValues, children, onSubmit }: FormWrapperProps) { - const methods = useForm({ - defaultValues, - }) - return ( - -
- {children} - -
-
- ) -} - -function selectOption(name: string, value: string) { - const selectTester = testUtilUser.createTester("Select", { - root: screen.getByTestId(name), - interactionType: "touch", - }) - - selectTester.selectOption({ option: value }) -} - -const testCases = [ - { - description: "date is set and submitted successfully", - defaultValue: "", - dateOfBirth: "1987-12-05", - expectedOutput: { - dateOfBirth: "1987-12-05", - year: 1987, - month: 12, - day: 5, - }, - }, - { - description: "sets default value and submits successfully", - defaultValue: "2000-01-01", - dateOfBirth: "", - expectedOutput: { - dateOfBirth: "2000-01-01", - year: 2000, - month: 1, - day: 1, - }, - }, - { - description: "accepts date exactly 18 years old", - defaultValue: "", - dateOfBirth: dt().subtract(18, "year").format("YYYY-MM-DD"), - expectedOutput: { - dateOfBirth: dt().subtract(18, "year").format("YYYY-MM-DD"), - }, - }, - { - description: "rejects date below 18 years old - by year", - defaultValue: "", - dateOfBirth: dt().subtract(17, "year").format("YYYY-MM-DD"), - expectedOutput: { - dateOfBirth: "", - }, - }, - { - description: "rejects date below 18 years old - by month", - defaultValue: "", - dateOfBirth: dt().subtract(18, "year").add(1, "month").format("YYYY-MM-DD"), - expectedOutput: { - dateOfBirth: "", - }, - }, - { - description: "rejects date below 18 years old - by day", - defaultValue: "", - dateOfBirth: dt().subtract(18, "year").add(1, "day").format("YYYY-MM-DD"), - expectedOutput: { - dateOfBirth: "", - }, - }, -] - -describe.skip("Date input", () => { - afterEach(cleanup) - - test.each(testCases)( - "$description", - async ({ defaultValue, dateOfBirth, expectedOutput }) => { - const user = userEvent.setup() - - render( - { - expect(data).toEqual(expectedOutput) - }} - > - - - ) - - const date = dt(dateOfBirth).toDate() - const year = date.getFullYear() - const month = date.getMonth() + 1 - const day = date.getDate() - - selectOption("year", year.toString()) - selectOption("month", getLocalizedMonthName(month, Lang.en)) - selectOption("day", day.toString()) - - const submitButton = screen.getByRole("button", { name: /submit/i }) - await user.click(submitButton) - } - ) -}) diff --git a/apps/scandic-web/tests/loginUtil.ts b/apps/scandic-web/tests/loginUtil.ts new file mode 100644 index 000000000..d7ca4bddf --- /dev/null +++ b/apps/scandic-web/tests/loginUtil.ts @@ -0,0 +1,44 @@ +import { expect, type Page } from "@playwright/test" + +const AUTH_URL_REGEX = + /https:\/\/.*\.scandichotels\.com\/authn\/authenticate\/scandic/i + +export async function performLogin({ + page, + username, + password, +}: { + page: Page + username: string + password: string +}) { + await page.goto("/en") + const loginLink = page.getByRole("link", { name: /log in\/join/i }) + await expect(loginLink).not.toBeDisabled() + + await loginLink.click() + + await expect(page).toHaveURL(AUTH_URL_REGEX) + + // Fill in the login form + + const usernameTextBox = page.getByRole("textbox", { + name: /email \/ membership number/i, + }) + await usernameTextBox.fill(username) + + const passwordTextBox = page.getByRole("textbox", { name: /password/i }) + await passwordTextBox.fill(password) + + const signInButton = page.getByRole("button", { name: /log in/i }) + await signInButton.click() + + const profileButton = await getProfileButton(page) + await expect(profileButton).toBeVisible() +} + +export async function getProfileButton(page: Page) { + return page.getByRole("button", { + name: /^\p{L}{2} hi \p{L}+!/iu, + }) +} diff --git a/apps/scandic-web/tests/profile/mybenefits.spec.ts b/apps/scandic-web/tests/profile/mybenefits.spec.ts new file mode 100644 index 000000000..70ef4f6f0 --- /dev/null +++ b/apps/scandic-web/tests/profile/mybenefits.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from "@playwright/test" + +import { getProfileButton, performLogin } from "../loginUtil" +import { testData } from "../testdata" + +test("can navigate to my benefits page", async ({ page }) => { + await performLogin({ + page, + username: testData.basicUser.membershipId, + password: testData.basicUser.password, + }) + + const profileButton = await getProfileButton(page) + profileButton.click() + const myStaysLink = page.getByRole("link", { name: /my benefits/i }) + await myStaysLink.click() + + const currentPerksAndBenefits = page.getByRole("heading", { + name: /current perks and benefits/i, + }) + await expect(currentPerksAndBenefits).toBeVisible() +}) diff --git a/apps/scandic-web/tests/profile/mypoints.spec.ts b/apps/scandic-web/tests/profile/mypoints.spec.ts new file mode 100644 index 000000000..50756ecbf --- /dev/null +++ b/apps/scandic-web/tests/profile/mypoints.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from "@playwright/test" + +import { getProfileButton, performLogin } from "../loginUtil" +import { testData } from "../testdata" + +test("can navigate to my points page", async ({ page }) => { + await performLogin({ + page, + username: testData.basicUser.membershipId, + password: testData.basicUser.password, + }) + + const profileButton = await getProfileButton(page) + profileButton.click() + const myPointsLink = page.getByRole("link", { name: /my points/i }) + await myPointsLink.click() + + const pointsToSpend = page.getByRole("heading", { name: /points to spend/i }) + await expect(pointsToSpend).toBeVisible() +}) diff --git a/apps/scandic-web/tests/profile/mystays.spec.ts b/apps/scandic-web/tests/profile/mystays.spec.ts new file mode 100644 index 000000000..614db2ab2 --- /dev/null +++ b/apps/scandic-web/tests/profile/mystays.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from "@playwright/test" + +import { getProfileButton, performLogin } from "../loginUtil" +import { testData } from "../testdata" + +test("can navigate to my stays page", async ({ page }) => { + await performLogin({ + page, + username: testData.basicUser.membershipId, + password: testData.basicUser.password, + }) + + const profileButton = await getProfileButton(page) + profileButton.click() + const myStaysLink = page.getByRole("link", { name: /my stays/i }) + await myStaysLink.click() + + const upcomingStays = page.getByRole("heading", { + name: /^upcoming stays$/i, + }) + await expect(upcomingStays).toBeVisible() +}) diff --git a/apps/scandic-web/tests/profile/overview.spec.ts b/apps/scandic-web/tests/profile/overview.spec.ts new file mode 100644 index 000000000..d621cfc76 --- /dev/null +++ b/apps/scandic-web/tests/profile/overview.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from "@playwright/test" + +import { getProfileButton, performLogin } from "../loginUtil" +import { testData } from "../testdata" + +test("can navigate to overview page", async ({ page }) => { + await performLogin({ + page, + username: testData.basicUser.membershipId, + password: testData.basicUser.password, + }) + + const profileButton = await getProfileButton(page) + profileButton.click() + const overviewLink = page.getByRole("link", { name: /overview/i }) + await overviewLink.click() + + const scandicLevel = page.getByText(/level \d+/i) + await expect(scandicLevel).toBeVisible() + + const membershipId = page.getByText(testData.basicUser.membershipId) + await expect(membershipId).toBeVisible() +}) diff --git a/apps/scandic-web/tests/testdata.ts b/apps/scandic-web/tests/testdata.ts new file mode 100644 index 000000000..7c9820405 --- /dev/null +++ b/apps/scandic-web/tests/testdata.ts @@ -0,0 +1,8 @@ +export const testData = { + basicUser: { + membershipId: "30812500328010", + password: "Test@12345", + firstName: "Test", + lastName: "Testsson", + }, +} diff --git a/apps/scandic-web/tests/utils.tsx b/apps/scandic-web/tests/utils.tsx deleted file mode 100644 index 6b34ebb63..000000000 --- a/apps/scandic-web/tests/utils.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { render, type RenderOptions } from "@testing-library/react" -import React, { type ReactElement } from "react" - -import { Lang } from "@scandic-hotels/common/constants/language" - -import messages from "@/i18n/dictionaries/en.json" -import ClientIntlProvider from "@/i18n/Provider" - -function AllTheProviders({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} - -const customRender = ( - ui: ReactElement, - options?: Omit -) => render(ui, { wrapper: AllTheProviders, ...options }) - -export * from "@testing-library/react" -export { customRender as render } diff --git a/yarn.lock b/yarn.lock index dcfcff0b8..5158053b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3045,7 +3045,7 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:^1.53.1": +"@playwright/test@npm:^1.53.1, @playwright/test@npm:^1.57.0": version: 1.57.0 resolution: "@playwright/test@npm:1.57.0" dependencies: @@ -5446,6 +5446,7 @@ __metadata: "@netlify/blobs": "npm:^8.1.0" "@netlify/functions": "npm:^3.0.0" "@netlify/plugin-nextjs": "npm:^5.14.4" + "@playwright/test": "npm:^1.57.0" "@radix-ui/react-slot": "npm:^1.2.2" "@react-aria/ssr": "npm:^3.9.8" "@react-aria/test-utils": "npm:1.0.0-alpha.8"