Add Playwright for end-to-end testing and update configuration
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Added Playwright as a dependency for end-to-end testing, including necessary scripts in package.json. - Created a Playwright configuration file to define test settings and browser options. - Implemented a new GitHub Actions workflow for automated end-to-end testing on push and pull request events. - Updated .gitignore to exclude Playwright test results and reports. - Added initial end-to-end tests for mail functionalities, including composing, sending, and searching messages.
This commit is contained in:
parent
bd50a4a54c
commit
9d0fb2766b
50
.github/workflows/e2e.yml
vendored
Normal file
50
.github/workflows/e2e.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
name: E2E
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, main]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: e2e-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
playwright:
|
||||
name: Playwright e2e
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
env:
|
||||
PLAYWRIGHT_BROWSERS_PATH: "0"
|
||||
|
||||
- name: Run e2e tests
|
||||
run: pnpm run e2e
|
||||
env:
|
||||
CI: true
|
||||
PLAYWRIGHT_BROWSERS_PATH: "0"
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 7
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -13,4 +13,10 @@ __v0_jsx-dev-runtime.ts
|
||||
# Common ignores
|
||||
node_modules
|
||||
.next/
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
56
e2e/helpers/mail-app.ts
Normal file
56
e2e/helpers/mail-app.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { expect, type Locator, type Page } from "@playwright/test"
|
||||
|
||||
export async function gotoInbox(page: Page) {
|
||||
await page.goto("/mail/inbox")
|
||||
await expect(page.locator("[data-email-row-id]").first()).toBeVisible()
|
||||
}
|
||||
|
||||
export function composeWindow(page: Page): Locator {
|
||||
return page.locator("[data-compose-window]").last()
|
||||
}
|
||||
|
||||
export async function openCompose(page: Page) {
|
||||
const composeButton = page
|
||||
.getByRole("button", { name: "Nouveau message" })
|
||||
.first()
|
||||
await composeButton.click()
|
||||
await expect(composeWindow(page)).toBeVisible()
|
||||
}
|
||||
|
||||
export async function fillCompose(
|
||||
page: Page,
|
||||
{
|
||||
to,
|
||||
subject,
|
||||
body,
|
||||
}: {
|
||||
to: string
|
||||
subject: string
|
||||
body: string
|
||||
}
|
||||
) {
|
||||
const compose = composeWindow(page)
|
||||
const toField = compose.locator('input[type="text"]').first()
|
||||
|
||||
await toField.fill(to)
|
||||
await toField.press("Enter")
|
||||
|
||||
await compose.getByPlaceholder("Objet").fill(subject)
|
||||
await compose.locator(".ProseMirror").click()
|
||||
await compose.locator(".ProseMirror").fill(body)
|
||||
}
|
||||
|
||||
export async function sendComposeNow(page: Page) {
|
||||
const compose = composeWindow(page)
|
||||
await compose.getByRole("button", { name: "Envoyer", exact: true }).click()
|
||||
await page.getByRole("button", { name: "Envoyer maintenant" }).click()
|
||||
await expect(page.getByText("Message envoyé")).toBeVisible()
|
||||
await expect(compose).toHaveCount(0)
|
||||
}
|
||||
|
||||
export async function scheduleComposeInOneHour(page: Page) {
|
||||
const compose = composeWindow(page)
|
||||
await compose.locator("button.rounded-r-full").click()
|
||||
await page.getByRole("menuitem", { name: "Envoyer dans une heure" }).click()
|
||||
await expect(page.getByText(/Ce mail sera envoyé le/)).toBeVisible()
|
||||
}
|
||||
74
e2e/mail-journeys.spec.ts
Normal file
74
e2e/mail-journeys.spec.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
import {
|
||||
fillCompose,
|
||||
gotoInbox,
|
||||
openCompose,
|
||||
scheduleComposeInOneHour,
|
||||
sendComposeNow,
|
||||
} from "./helpers/mail-app"
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.clear()
|
||||
window.sessionStorage.clear()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("mail journeys (mock data)", () => {
|
||||
test("reads a message from inbox", async ({ page }) => {
|
||||
await gotoInbox(page)
|
||||
|
||||
await page.locator('[data-email-row-id="1"]').click()
|
||||
|
||||
await expect(page).toHaveURL(/\/mail\/inbox\/message\/1/)
|
||||
await expect(page.getByTitle("Sujet du message")).toBeVisible()
|
||||
await expect(page.getByLabel("Répondre").first()).toBeVisible()
|
||||
})
|
||||
|
||||
test("composes and sends a message", async ({ page }) => {
|
||||
const subject = `E2E send ${Date.now()}`
|
||||
|
||||
await gotoInbox(page)
|
||||
await openCompose(page)
|
||||
await fillCompose(page, {
|
||||
to: "test@example.com",
|
||||
subject,
|
||||
body: "Playwright send journey",
|
||||
})
|
||||
await sendComposeNow(page)
|
||||
})
|
||||
|
||||
test("schedules a message for later", async ({ page }) => {
|
||||
const subject = `E2E schedule ${Date.now()}`
|
||||
|
||||
await gotoInbox(page)
|
||||
await openCompose(page)
|
||||
await fillCompose(page, {
|
||||
to: "scheduled@example.com",
|
||||
subject,
|
||||
body: "Playwright schedule journey",
|
||||
})
|
||||
await scheduleComposeInOneHour(page)
|
||||
|
||||
await expect(page.getByText(/Ce mail sera envoyé le/)).toBeVisible()
|
||||
await page.waitForFunction(
|
||||
(expectedSubject) => {
|
||||
const raw = localStorage.getItem("ultimail-scheduled-state")
|
||||
return raw?.includes(expectedSubject) ?? false
|
||||
},
|
||||
subject,
|
||||
{ timeout: 10_000 }
|
||||
)
|
||||
})
|
||||
|
||||
test("searches messages", async ({ page }) => {
|
||||
await gotoInbox(page)
|
||||
|
||||
const search = page.getByPlaceholder("Rechercher dans les messages")
|
||||
await search.fill("Uber")
|
||||
await search.press("Enter")
|
||||
|
||||
await expect(page).toHaveURL(/\/mail\/search/)
|
||||
await expect(page.locator("[data-email-row-id]").first()).toContainText("Uber")
|
||||
})
|
||||
})
|
||||
@ -2,9 +2,18 @@
|
||||
* Copyright (c) 2026 Eliott Guillaumin
|
||||
* All rights reserved.
|
||||
*/
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const projectRoot = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
output: "standalone",
|
||||
outputFileTracingRoot: projectRoot,
|
||||
turbopack: {
|
||||
root: projectRoot,
|
||||
},
|
||||
allowedDevOrigins: ['192.168.0.20', '127.0.0.1', 'localhost', '100.120.4.66'],
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint .",
|
||||
"e2e": "playwright test",
|
||||
"e2e:ui": "playwright test --ui",
|
||||
"e2e:headed": "playwright test --headed",
|
||||
"e2e:server": "pnpm build && cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/static && node .next/standalone/server.js",
|
||||
"expose": "source .env && cloudflared tunnel --loglevel error run --token $CLOUDFLARE_TUNNEL_TOKEN",
|
||||
"brand:raster": "node scripts/rasterize-ultimail-brand.mjs",
|
||||
"brand:vectorize": "node scripts/vectorize-ultimail-brand.mjs",
|
||||
@ -86,6 +90,7 @@
|
||||
"zustand": "^5.0.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@tailwindcss/postcss": "^4.2.0",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
|
||||
32
playwright.config.ts
Normal file
32
playwright.config.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { defineConfig, devices } from "@playwright/test"
|
||||
|
||||
const PORT = Number(process.env.PLAYWRIGHT_PORT ?? 3099)
|
||||
const baseURL = `http://127.0.0.1:${PORT}`
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./e2e",
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: process.env.CI ? "github" : "list",
|
||||
timeout: 60_000,
|
||||
use: {
|
||||
baseURL,
|
||||
trace: "on-first-retry",
|
||||
screenshot: "only-on-failure",
|
||||
video: "retain-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: `HOSTNAME=127.0.0.1 PORT=${PORT} pnpm run e2e:server`,
|
||||
url: baseURL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 300_000,
|
||||
},
|
||||
})
|
||||
@ -145,7 +145,7 @@ importers:
|
||||
version: 3.23.2
|
||||
'@vercel/analytics':
|
||||
specifier: 1.6.1
|
||||
version: 1.6.1(next@16.2.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
|
||||
version: 1.6.1(next@16.2.6(@playwright/test@1.60.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.24(postcss@8.5.6)
|
||||
@ -184,7 +184,7 @@ importers:
|
||||
version: 0.564.0(react@19.2.4)
|
||||
next:
|
||||
specifier: 16.2.6
|
||||
version: 16.2.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
version: 16.2.6(@playwright/test@1.60.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@ -222,6 +222,9 @@ importers:
|
||||
specifier: ^5.0.13
|
||||
version: 5.0.13(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
||||
devDependencies:
|
||||
'@playwright/test':
|
||||
specifier: ^1.52.0
|
||||
version: 1.60.0
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@ -522,6 +525,11 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@playwright/test@1.60.0':
|
||||
resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@radix-ui/number@1.1.1':
|
||||
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
||||
|
||||
@ -1655,6 +1663,11 @@ packages:
|
||||
fraction.js@5.3.4:
|
||||
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fuse.js@7.3.0:
|
||||
resolution: {integrity: sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==}
|
||||
engines: {node: '>=10'}
|
||||
@ -1816,6 +1829,16 @@ packages:
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
playwright-core@1.60.0:
|
||||
resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.60.0:
|
||||
resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
@ -2289,6 +2312,10 @@ snapshots:
|
||||
'@next/swc-win32-x64-msvc@16.2.6':
|
||||
optional: true
|
||||
|
||||
'@playwright/test@1.60.0':
|
||||
dependencies:
|
||||
playwright: 1.60.0
|
||||
|
||||
'@radix-ui/number@1.1.1': {}
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
@ -3292,9 +3319,9 @@ snapshots:
|
||||
|
||||
'@types/use-sync-external-store@0.0.6': {}
|
||||
|
||||
'@vercel/analytics@1.6.1(next@16.2.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)':
|
||||
'@vercel/analytics@1.6.1(next@16.2.6(@playwright/test@1.60.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)':
|
||||
optionalDependencies:
|
||||
next: 16.2.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
next: 16.2.6(@playwright/test@1.60.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react: 19.2.4
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
@ -3432,6 +3459,9 @@ snapshots:
|
||||
|
||||
fraction.js@5.3.4: {}
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fuse.js@7.3.0: {}
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
@ -3521,7 +3551,7 @@ snapshots:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
next@16.2.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
next@16.2.6(@playwright/test@1.60.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@next/env': 16.2.6
|
||||
'@swc/helpers': 0.5.15
|
||||
@ -3540,6 +3570,7 @@ snapshots:
|
||||
'@next/swc-linux-x64-musl': 16.2.6
|
||||
'@next/swc-win32-arm64-msvc': 16.2.6
|
||||
'@next/swc-win32-x64-msvc': 16.2.6
|
||||
'@playwright/test': 1.60.0
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@ -3553,6 +3584,14 @@ snapshots:
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
playwright-core@1.60.0: {}
|
||||
|
||||
playwright@1.60.0:
|
||||
dependencies:
|
||||
playwright-core: 1.60.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.4.31:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user