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() {
-