diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..37d206d --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: pnpm install + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run Playwright tests + run: pnpm exec playwright test + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/leaky-ships/.gitignore b/leaky-ships/.gitignore index 1c20691..8f793d8 100644 --- a/leaky-ships/.gitignore +++ b/leaky-ships/.gitignore @@ -1,7 +1,5 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -__tests__/screenshots/* - # logs /log @@ -43,3 +41,8 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# playwright +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/leaky-ships/__tests__/auth.js b/leaky-ships/__tests__/auth.js deleted file mode 100644 index 69e4c2d..0000000 --- a/leaky-ships/__tests__/auth.js +++ /dev/null @@ -1,83 +0,0 @@ -describe("Check Azure AD auth", () => { - const callbackUrl = process.env.NEXTAUTH_URL + "/" - - it("Login process...", async () => { - let redirected = false - let thirdParty = false - - page.on("framenavigated", (frame) => { - if (redirected) return - const frameUrl = frame.url() - // console.log("Window Location Changed:", frameUrl) - if (frameUrl === callbackUrl) redirected = true - }) - - try { - await page.goto(callbackUrl + "signin") - await page.waitForSelector("#microsoft") - await page.click("#microsoft") - - thirdParty = true - - await page.waitForNavigation() - - await page.waitForSelector('input[type="email"]') - const emailInput = await page.$('input[type="email"]') - await emailInput.type(process.env.AUTH_EMAIL) - - await page.waitForSelector('input[value="Next"]') - const nextInput = await page.$('input[value="Next"]') - await nextInput.click() - - await page.waitForSelector('input[type="password"]') - const passwordInput = await page.$('input[type="password"]') - await passwordInput.type(process.env.AUTH_PW) - - await page.waitForSelector('input[value="Sign in"]') - const signinInput = await page.$('input[value="Sign in"]') - await signinInput.click() - - await page.waitForSelector('input[value="No"]') - const noInput = await page.$('input[value="No"]') - await noInput.click() - - await page.waitForFunction(`window.location.href === '${callbackUrl}'`) - } catch (e) { - if (!redirected || thirdParty) throw e - } - }, 60000) - - it("Is logged in", async () => { - await page.goto(callbackUrl + "signin") - await page.waitForFunction(`window.location.href === '${callbackUrl}'`) - }, 30000) - - it("Is logged out", async () => { - await page.goto( - "https://login.microsoftonline.com/common/oauth2/v2.0/logout", - ) - - await page.waitForSelector(`div[data-test-id="${process.env.AUTH_EMAIL}"]`) - const signoutDiv = await page.$( - `div[data-test-id="${process.env.AUTH_EMAIL}"]`, - ) - await signoutDiv.click() - - await page.waitForFunction( - `window.location.href === 'https://login.microsoftonline.com/common/oauth2/v2.0/logoutsession'`, - ) - - // Wait for the element to be visible in the page - await page.waitForSelector("#login_workload_logo_text") - // Get the element handle - const elementHandle = await page.$("#login_workload_logo_text") - // Get the inner text of the element - const innerText = await page.evaluate( - (element) => element.innerText, - elementHandle, - ) - - // Assert that the inner text matches the expected text - expect(innerText.trim()).toBe("You signed out of your account") - }, 30000) -}) diff --git a/leaky-ships/__tests__/email.js b/leaky-ships/__tests__/email.js deleted file mode 100644 index 541e02a..0000000 --- a/leaky-ships/__tests__/email.js +++ /dev/null @@ -1,57 +0,0 @@ -const { PrismaClient } = require("@prisma/client") -const prisma = new PrismaClient() -const { createHash, randomBytes } = require("crypto") - -describe("Check Email auth", () => { - const callbackUrl = process.env.NEXTAUTH_URL + "/" - const player1Email = "player1@example.com" - - it("Email login process...", async () => { - await page.goto(callbackUrl + "signin") - - await page.waitForSelector('input[type="email"]') - const emailInput = await page.$('input[type="email"]') - await emailInput.type(player1Email) - - await page.click('button[type="submit"]') - - await page.waitForFunction( - `window.location.href === '${callbackUrl}api/auth/verify-request?provider=email&type=email'`, - ) - }, 30000) - - it("Verify Email...", async () => { - const token = randomBytes(32).toString("hex") - - const hash = createHash("sha256") - // Prefer provider specific secret, but use default secret if none specified - .update(`${token}${process.env.NEXTAUTH_SECRET}`) - .digest("hex") - - // Use Prisma to fetch the latest token for the email - const latestToken = await prisma.VerificationToken.findFirst({ - where: { identifier: player1Email }, - orderBy: { expires: "desc" }, - }) - await prisma.VerificationToken.update({ - where: { - identifier_token: { - identifier: player1Email, - token: latestToken.token, - }, - }, - data: { token: hash }, - }) - - const params = new URLSearchParams({ - callbackUrl, - token, - email: player1Email, - }) - const url = callbackUrl + "api/auth/callback/email?" + params - - await page.goto(url) - - await page.waitForFunction(`window.location.href === '${callbackUrl}'`) - }, 30000) -}) diff --git a/leaky-ships/build-start-test.sh b/leaky-ships/build-start-test.sh deleted file mode 100755 index 40416a9..0000000 --- a/leaky-ships/build-start-test.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# Build the project -pnpm run build - -# Function to kill the server process -function kill_server { - local server_pid=$(lsof -i :3000 -t) - if [[ -n $server_pid ]]; then - echo "Killing server..." $server_pid - kill -15 $server_pid - fi -} - -# Function to run the tests -function run_tests { - pnpm test -} - -# Start the server in the background -pnpm run start & - -# Capture exit signals and execute the kill_server function -trap kill_server EXIT ERR - -# Run the tests -run_tests diff --git a/leaky-ships/e2e/auth.spec.ts b/leaky-ships/e2e/auth.spec.ts new file mode 100644 index 0000000..5e40ddb --- /dev/null +++ b/leaky-ships/e2e/auth.spec.ts @@ -0,0 +1,85 @@ +import { expect, test, type BrowserContext, type Page } from "@playwright/test" + +const callbackUrl = process.env.NEXTAUTH_URL + "/" +let context: BrowserContext +let page: Page + +test.describe.serial("Check Azure AD auth", () => { + test.beforeAll(async ({ browser }) => { + context = await browser.newContext() + page = await context.newPage() + }) + + test.afterAll(async () => { + await context.close() + }) + + test("Login process...", async ({ browser }) => { + await page.goto(callbackUrl + "signin") + + await page + .getByRole("button", { name: "Microsoft_icon Sign in with Microsoft" }) + .click() + + const emailLocator = page.locator( + `[data-test-id="${process.env.AUTH_EMAIL ?? ""}"]`, + ) + if (await emailLocator.isVisible()) { + await emailLocator.click() + await new Promise((resolve) => setTimeout(resolve, 1000)) + } else { + console.log( + "The email locator is not present on the page. Skipping this step.", + ) + // Optionally, you can throw an error, fail the test, or take any other desired action here. + } + + await page + .getByLabel("someone@example.com") + .fill(process.env.AUTH_EMAIL ?? "") + + await page.getByRole("button", { name: "Next" }).click() + await new Promise((resolve) => setTimeout(resolve, 1000)) + + await page.getByLabel("Password").fill(process.env.AUTH_PW ?? "") + await page.getByRole("button", { name: "Sign in" }).click() + await new Promise((resolve) => setTimeout(resolve, 1000)) + + await page.getByRole("button", { name: "No" }).click({}) + await new Promise((resolve) => setTimeout(resolve, 1000)) + + await page.waitForURL( + callbackUrl + (browser.browserType().name() === "webkit" ? "#" : ""), + ) + }) + + test("Is logged in", async () => { + await page.goto(callbackUrl + "signin") + await page.waitForURL(callbackUrl) + expect(await page.screenshot()).toMatchSnapshot("1.png") + }) + + test("Is logged out", async () => { + await page.goto( + "https://login.microsoftonline.com/common/oauth2/v2.0/logout", + ) + + await page.waitForLoadState("domcontentloaded") + + const emailLocator = page.locator( + `[data-test-id="${process.env.AUTH_EMAIL ?? ""}"]`, + ) + if (await emailLocator.isVisible()) { + await emailLocator.click() + } else { + console.log( + "The email locator is not present on the page. Skipping this step.", + ) + // Optionally, you can throw an error, fail the test, or take any other desired action here. + } + + await page + .getByRole("heading", { name: "You signed out of your account" }) + .click() + }) +}) diff --git a/leaky-ships/e2e/auth.spec.ts-snapshots/1-chromium-linux.png b/leaky-ships/e2e/auth.spec.ts-snapshots/1-chromium-linux.png new file mode 100644 index 0000000..85080d3 Binary files /dev/null and b/leaky-ships/e2e/auth.spec.ts-snapshots/1-chromium-linux.png differ diff --git a/leaky-ships/e2e/auth.spec.ts-snapshots/1-firefox-linux.png b/leaky-ships/e2e/auth.spec.ts-snapshots/1-firefox-linux.png new file mode 100644 index 0000000..a02236d Binary files /dev/null and b/leaky-ships/e2e/auth.spec.ts-snapshots/1-firefox-linux.png differ diff --git a/leaky-ships/e2e/auth.spec.ts-snapshots/1-webkit-linux.png b/leaky-ships/e2e/auth.spec.ts-snapshots/1-webkit-linux.png new file mode 100644 index 0000000..e3e7a23 Binary files /dev/null and b/leaky-ships/e2e/auth.spec.ts-snapshots/1-webkit-linux.png differ diff --git a/leaky-ships/e2e/email.spec.ts b/leaky-ships/e2e/email.spec.ts new file mode 100644 index 0000000..9ec7905 --- /dev/null +++ b/leaky-ships/e2e/email.spec.ts @@ -0,0 +1,72 @@ +import { + test, + type Browser, + type BrowserContext, + type Page, +} from "@playwright/test" +import { createHash, randomBytes } from "crypto" +import prisma from "../lib/prisma" + +const callbackUrl = process.env.NEXTAUTH_URL + "/" +const player1Email = (browser: Browser) => + browser.browserType().name() + "-player-1@example.com" + +let context: BrowserContext +let page: Page + +test.describe.serial("Check Email auth", () => { + test.beforeAll(async ({ browser }) => { + context = await browser.newContext() + page = await context.newPage() + }) + + test.afterAll(async () => { + await context.close() + }) + + test("Email login process...", async ({ browser }) => { + await page.goto(callbackUrl + "signin") + + await page.getByPlaceholder("user@example.com").fill(player1Email(browser)) + await page.getByRole("button", { name: "Sign in with Email" }).click() + + await page.waitForURL( + callbackUrl + "api/auth/verify-request?provider=email&type=email", + ) + }) + + test("Verify Email...", async ({ browser }) => { + const token = randomBytes(32).toString("hex") + + const hash = createHash("sha256") + // Prefer provider specific secret, but use default secret if none specified + .update(`${token}${process.env.NEXTAUTH_SECRET}`) + .digest("hex") + + // Use Prisma to fetch the latest token for the email + const latestToken = await prisma.verificationToken.findFirst({ + where: { identifier: player1Email(browser) }, + orderBy: { expires: "desc" }, + }) + await prisma.verificationToken.update({ + where: { + identifier_token: { + identifier: player1Email(browser), + token: latestToken?.token ?? "", + }, + }, + data: { token: hash }, + }) + + const params = new URLSearchParams({ + callbackUrl, + token, + email: player1Email(browser), + }) + const url = callbackUrl + "api/auth/callback/email?" + params + + await page.goto(url) + + await page.waitForLoadState("domcontentloaded") + }) +}) diff --git a/leaky-ships/jest.config.js b/leaky-ships/jest.config.js deleted file mode 100644 index 9c24ba5..0000000 --- a/leaky-ships/jest.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - verbose: true, - preset: "jest-puppeteer", - setupFiles: ["dotenv/config"], -} diff --git a/leaky-ships/kill-server.sh b/leaky-ships/kill-server.sh new file mode 100755 index 0000000..e8f8443 --- /dev/null +++ b/leaky-ships/kill-server.sh @@ -0,0 +1,5 @@ +server_pid=$(lsof -i :3000 -t) +if [[ -n $server_pid ]]; then + echo "Killing server..." $server_pid + kill -9 $server_pid +fi diff --git a/leaky-ships/package.json b/leaky-ships/package.json index 7dd2463..62c6a46 100644 --- a/leaky-ships/package.json +++ b/leaky-ships/package.json @@ -7,8 +7,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "jest -w 1", - "build-start-test": "./build-start-test.sh" + "test": "pnpm playwright test --ui" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.4.0", @@ -46,6 +45,7 @@ "zustand": "^4.3.9" }, "devDependencies": { + "@playwright/test": "^1.36.2", "@total-typescript/ts-reset": "^0.3.7", "@types/node": "^18.17.0", "@types/react": "^18.2.15", @@ -54,8 +54,6 @@ "autoprefixer": "^10.4.14", "dotenv": "^16.3.1", "eslint-config-prettier": "^8.8.0", - "jest": "^29.6.1", - "jest-puppeteer": "^9.0.0", "postcss": "^8.4.27", "prettier": "^3.0.0", "prettier-plugin-organize-imports": "^3.2.3", diff --git a/leaky-ships/pages/index.tsx b/leaky-ships/pages/index.tsx index 7a5d478..4018b5d 100644 --- a/leaky-ships/pages/index.tsx +++ b/leaky-ships/pages/index.tsx @@ -11,7 +11,7 @@ export default function Home() {
-