diff --git a/.github/actions/e2e-setup/action.yml b/.github/actions/e2e-setup/action.yml new file mode 100644 index 00000000..0a1c4abd --- /dev/null +++ b/.github/actions/e2e-setup/action.yml @@ -0,0 +1,21 @@ +name: 🎭 E2E Test Setup +description: 'Setup Playwright and install browsers for E2E testing' + +runs: + using: 'composite' + steps: + - name: πŸ’Ύ Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: 🎭 Install Playwright browsers + run: pnpm e2e:install + shell: bash + + - name: πŸ“‹ Verify Playwright installation + run: pnpm exec playwright --version + shell: bash diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..a62be287 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,63 @@ +name: 🎭 Playwright E2E + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +concurrency: + group: playwright-e2e-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + NODE_ENV: test + CI: true + E2E_BASE_URL: http://localhost:3000 + +jobs: + e2e: + runs-on: ubuntu-24.04 + container: mcr.microsoft.com/playwright:v1.57.0-noble + timeout-minutes: 60 + steps: + - name: πŸ“₯ Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + + - name: πŸ’¨ Cache turbo + uses: actions/cache@v4 + with: + path: | + .turbo + key: ${{ runner.os }}-turbo-${{ hashFiles('pnpm-lock.yaml', 'turbo.json') }} + restore-keys: | + ${{ runner.os }}-turbo- + + - name: πŸ’» Node setup + uses: ./.github/actions/node-setup + + - name: πŸ—οΈ Build apps + run: pnpm build + shell: bash + + - name: 🎭 E2E setup + uses: ./.github/actions/e2e-setup + + - name: πŸ§ͺ Run E2E (HTML report) + run: pnpm e2e + env: + PWDEBUG: '0' + shell: bash + + - name: πŸ“€ Upload Playwright artifacts + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: playwright-artifacts + path: | + apps/*-e2e/playwright-report/ + apps/*-e2e/test-results/ + apps/*-e2e/reports/ + if-no-files-found: ignore diff --git a/apps/frontend-e2e/.gitignore b/apps/frontend-e2e/.gitignore new file mode 100644 index 00000000..232503a6 --- /dev/null +++ b/apps/frontend-e2e/.gitignore @@ -0,0 +1,7 @@ +# Playwright artifacts (keep snapshots committed) +/test-results/ +/playwright-report/ +/reports/ +/playwright/.cache/ +.last-run.json +/artifacts/ diff --git a/apps/frontend-e2e/.prettierignore b/apps/frontend-e2e/.prettierignore new file mode 100644 index 00000000..04b7ce04 --- /dev/null +++ b/apps/frontend-e2e/.prettierignore @@ -0,0 +1,10 @@ +# Dependencies +node_modules/ + +# Cache +.turbo/ + +# Playwright artifacts +**/test-results/ +**/playwright-report/ +**/reports/ diff --git a/apps/frontend-e2e/.prettierrc.js b/apps/frontend-e2e/.prettierrc.js new file mode 100644 index 00000000..c87f92a9 --- /dev/null +++ b/apps/frontend-e2e/.prettierrc.js @@ -0,0 +1,6 @@ +const baseConfig = require('@infinum/configs/prettier'); + +/** @type {import('prettier').Config} */ +module.exports = { + ...baseConfig, +}; diff --git a/apps/frontend-e2e/README.md b/apps/frontend-e2e/README.md new file mode 100644 index 00000000..8e7ffbb4 --- /dev/null +++ b/apps/frontend-e2e/README.md @@ -0,0 +1,7 @@ +# E2E Testing with Playwright + +This package contains end-to-end tests for the frontend application using Playwright. + +For setup, commands, artifact locations (playwright-report, reports, test-results), and debugging notes, see the central guide: + +[`documentation/E2E Testing.md`](../../documentation/E2E%20Testing.md) diff --git a/apps/frontend-e2e/eslint.config.mjs b/apps/frontend-e2e/eslint.config.mjs new file mode 100644 index 00000000..c2c165c4 --- /dev/null +++ b/apps/frontend-e2e/eslint.config.mjs @@ -0,0 +1,18 @@ +import baseConfig from '@infinum/configs/eslint/base'; +import playwrightConfig from '@infinum/configs/eslint/playwright'; +import typescriptConfig from '@infinum/configs/eslint/typescript'; + +export default [ + ...baseConfig, + ...typescriptConfig, + ...playwrightConfig, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/apps/frontend-e2e/package.json b/apps/frontend-e2e/package.json new file mode 100644 index 00000000..b36d5384 --- /dev/null +++ b/apps/frontend-e2e/package.json @@ -0,0 +1,28 @@ +{ + "name": "@infinum/frontend-e2e", + "version": "0.0.0", + "private": true, + "scripts": { + "check-licenses": "node ../../scripts/check-licenses-workspace.js", + "clean": "rm -rf node_modules .turbo .eslintcache", + "e2e": "playwright test --reporter=html", + "e2e:install": "playwright install", + "e2e:report": "playwright show-report", + "lint": "eslint . --cache", + "lint:fix": "eslint . --cache --fix", + "prettier:check": "prettier --check .", + "prettier:fix": "prettier --write ." + }, + "devDependencies": { + "@axe-core/playwright": "catalog:", + "@infinum/configs": "workspace:*", + "@infinum/e2e-utils": "workspace:*", + "@playwright/test": "catalog:", + "@types/node": "catalog:", + "axe-core": "catalog:", + "axe-html-reporter": "catalog:", + "eslint": "catalog:", + "prettier": "catalog:", + "typescript": "catalog:" + } +} diff --git a/apps/frontend-e2e/pages/login.ts b/apps/frontend-e2e/pages/login.ts new file mode 100644 index 00000000..5506f7f3 --- /dev/null +++ b/apps/frontend-e2e/pages/login.ts @@ -0,0 +1,38 @@ +import { Page, Locator } from '@playwright/test'; +import { BasePage } from '@infinum/e2e-utils/pages'; + +export class LoginPage extends BasePage { + readonly emailInput: Locator; + readonly passwordInput: Locator; + readonly submitButton: Locator; + readonly errorMessage: Locator; + + constructor(page: Page) { + super(page); + + // Semantic locators + this.emailInput = page.getByLabel('Email'); + this.passwordInput = page.getByLabel('Password'); + this.submitButton = page.getByRole('button', { name: 'Sign in' }); + this.errorMessage = page.getByTestId('login-error'); + } + + async goto() { + await this.navigateTo('/en/login'); + await this.waitForLoad(); + } + + async login(email: string, password: string) { + // Check if form elements are available + await this.waitForVisible(this.emailInput); + await this.waitForVisible(this.passwordInput); + await this.waitForVisible(this.submitButton); + + await this.emailInput.fill(email); + await this.passwordInput.fill(password); + await this.submitButton.click(); + + // Wait for navigation to complete + await this.waitForNavigation(); + } +} diff --git a/apps/frontend-e2e/playwright.config.ts b/apps/frontend-e2e/playwright.config.ts new file mode 100644 index 00000000..8fab27bc --- /dev/null +++ b/apps/frontend-e2e/playwright.config.ts @@ -0,0 +1,26 @@ +import baseConfig from '@infinum/configs/playwright/base'; +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + ...baseConfig, + testDir: './tests', + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + baseURL: process.env.E2E_BASE_URL ?? 'http://localhost:3000', + }, + }, + ], + webServer: process.env.CI + ? { + command: 'pnpm --filter @infinum/frontend start', + port: 3000, + reuseExistingServer: !process.env.CI, + env: { + NODE_OPTIONS: '', + }, + } + : undefined, +}); diff --git a/apps/frontend-e2e/tests/home-axe.e2e.spec.ts b/apps/frontend-e2e/tests/home-axe.e2e.spec.ts new file mode 100644 index 00000000..ed80391e --- /dev/null +++ b/apps/frontend-e2e/tests/home-axe.e2e.spec.ts @@ -0,0 +1,45 @@ +import { expect, test } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; +import { createHtmlReport } from 'axe-html-reporter'; +import { a11yRules, saveHtmlReport, attachScreenshot, attachJson, getScreenshotPath } from '@infinum/e2e-utils'; + +export const urlsToCheck = [ + { + url: '/en', + name: 'home-en', + }, +]; + +const reportsDir = 'reports/a11y'; + +test.describe('Accessibility', () => { + urlsToCheck.forEach(({ url, name }) => { + test(`should check: ${url}`, async ({ page }, testInfo) => { + const currentBrowser = testInfo.project.name; + const reportName = `${name}.html`; + + await page.goto(url); + + const accessibilityScanResults = await new AxeBuilder({ page }).withTags(a11yRules).analyze(); + expect(accessibilityScanResults.violations, 'Expected zero a11y violations').toHaveLength(0); + + const screenshotName = `${currentBrowser}-${name}`; + const screenshot = await page.screenshot({ + path: getScreenshotPath(currentBrowser, name), + type: 'png', + }); + + await attachScreenshot(testInfo, screenshot, screenshotName); + await attachJson(testInfo, accessibilityScanResults, 'accessibility-scan-results'); + + const axeHtmlReport = createHtmlReport({ + results: accessibilityScanResults, + options: { + customSummary: `Browser: ${currentBrowser}`, + }, + }); + + saveHtmlReport(axeHtmlReport, currentBrowser, reportName, reportsDir); + }); + }); +}); diff --git a/apps/frontend-e2e/tests/home.e2e.spec.ts b/apps/frontend-e2e/tests/home.e2e.spec.ts new file mode 100644 index 00000000..7a44f7d8 --- /dev/null +++ b/apps/frontend-e2e/tests/home.e2e.spec.ts @@ -0,0 +1,39 @@ +import { test as base, expect } from '@playwright/test'; +import { LoginPage } from '../pages/login'; + +export const test = base.extend<{ + homePage: { goto: () => Promise }; +}>({ + homePage: async ({ page }, use) => { + const loginPage = new LoginPage(page); + await loginPage.goto(); + await loginPage.login('user@example.com', 'password123'); + + await page.waitForURL('/en'); + + const homePage = { + goto: async () => { + await page.goto('/en'); + }, + }; + + await use(homePage); + }, +}); + +test.describe('Home Page', () => { + test('should match home-page-content screenshot', async ({ page, browserName, homePage }) => { + await homePage.goto(); + + // run `playwright test --update-snapshots` to update the screenshot + const screenshotPath = `reports/screenshots/${browserName}-home-en.png`; // Using relative path for snapshot comparison + + const homePageContent = page.getByTestId('home-page-content'); + await expect(homePageContent).toBeVisible(); + + // do a visual regression check with snapshot (allow minor AA/font variance in CI) + await expect(homePageContent).toHaveScreenshot(screenshotPath, { + maxDiffPixelRatio: 0.015, + }); + }); +}); diff --git a/apps/frontend-e2e/tests/home.e2e.spec.ts-snapshots/reports-screenshots-chromium-home-en-chromium-linux.png b/apps/frontend-e2e/tests/home.e2e.spec.ts-snapshots/reports-screenshots-chromium-home-en-chromium-linux.png new file mode 100644 index 00000000..417590db Binary files /dev/null and b/apps/frontend-e2e/tests/home.e2e.spec.ts-snapshots/reports-screenshots-chromium-home-en-chromium-linux.png differ diff --git a/apps/frontend-e2e/tests/login.e2e.spec.ts b/apps/frontend-e2e/tests/login.e2e.spec.ts new file mode 100644 index 00000000..833c7784 --- /dev/null +++ b/apps/frontend-e2e/tests/login.e2e.spec.ts @@ -0,0 +1,33 @@ +import { test as base, expect } from '@playwright/test'; + +import { LoginPage } from '../pages/login'; + +export const test = base.extend<{ + loginPage: LoginPage; +}>({ + loginPage: async ({ page }, use) => { + const loginPage = new LoginPage(page); + await use(loginPage); // pass fixture to tests + }, +}); + +test.describe('LoginForm tests', () => { + test('shows error with wrong password', async ({ page, loginPage }) => { + await loginPage.goto(); + await loginPage.login('user@example.com', 'invalid'); + + const error = page.getByText('Invalid password'); + await expect(error).toBeVisible(); + + // Optionally, assert the text explicitly + await expect(error).toHaveText('Invalid password'); + }); + + test('successfully logs in with correct password', async ({ loginPage, page }) => { + await loginPage.goto(); + await loginPage.login('user@example.com', 'valid'); + + await page.waitForURL('/en', { timeout: 2000 }); + await expect(page.getByText('Logged in')).toBeVisible(); + }); +}); diff --git a/apps/frontend-e2e/tests/multi-device.e2e.spec.ts b/apps/frontend-e2e/tests/multi-device.e2e.spec.ts new file mode 100644 index 00000000..a1854bea --- /dev/null +++ b/apps/frontend-e2e/tests/multi-device.e2e.spec.ts @@ -0,0 +1,69 @@ +import { Browser, expect, test } from '@playwright/test'; +import { LoginPage } from '../pages/login'; +import { viewports, gotoWithRetry, waitForUrl, getScreenshotPath } from '@infinum/e2e-utils'; + +async function createContext( + browser: Browser, + email: string, + password: string, + viewport: { width: number; height: number } +) { + const context = await browser.newContext({ viewport }); + const page = await context.newPage(); + + // Test basic connectivity + try { + await gotoWithRetry(page, '/'); + await page.title(); + } catch (error) { + throw error; + } + + const login = new LoginPage(page); + await login.goto(); + await login.login(email, password); + + try { + await waitForUrl(page, '/en', 30000); + } catch (error) { + // Take screenshot for debugging + await page.screenshot({ path: `debug-${email}-failed.png` }); + throw error; + } + + await expect(page.locator('text=Logged in')).toBeVisible(); + return { context, page }; +} + +test.describe('Browser contexts & multiple devices', () => { + test('should allow multiple users in separate contexts with different viewports and positions', async ({ + browser, + }) => { + // Desktop user + const desktop = await createContext(browser, 'user1@example.com', 'password123', viewports.desktop); + + // Tablet user (e.g., iPad) + const tablet = await createContext(browser, 'user3@example.com', 'password123', viewports.tablet); + + // Mobile user + const mobile = await createContext(browser, 'user2@example.com', 'password123', viewports.mobile); + + // Screenshots for visual verification + assertions + const desktopShot = await desktop.page.screenshot({ path: getScreenshotPath('desktop', 'logged-in') }); + const tabletShot = await tablet.page.screenshot({ path: getScreenshotPath('tablet', 'logged-in') }); + const mobileShot = await mobile.page.screenshot({ path: getScreenshotPath('mobile', 'logged-in') }); + + await expect(desktop.page.locator('text=Logged in')).toBeVisible(); + await expect(tablet.page.locator('text=Logged in')).toBeVisible(); + await expect(mobile.page.locator('text=Logged in')).toBeVisible(); + + // Attach for debugging/reporting + await test.info().attach('desktop-logged-in', { body: desktopShot, contentType: 'image/png' }); + await test.info().attach('tablet-logged-in', { body: tabletShot, contentType: 'image/png' }); + await test.info().attach('mobile-logged-in', { body: mobileShot, contentType: 'image/png' }); + + await desktop.context.close(); + await tablet.context.close(); + await mobile.context.close(); + }); +}); diff --git a/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts b/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts new file mode 100644 index 00000000..f4b56446 --- /dev/null +++ b/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts @@ -0,0 +1,72 @@ +import { test as base, expect, Page } from '@playwright/test'; +import { LoginPage } from '../pages/login'; + +export const test = base.extend<{ authedPage: Page }>({ + authedPage: async ({ page }, use) => { + // Start in a deterministic theme to avoid flakiness across headless/headed. + await page.addInitScript(() => { + if (!localStorage.getItem('theme')) { + localStorage.setItem('theme', 'light'); + } + }); + + const loginPage = new LoginPage(page); + await loginPage.goto(); + await loginPage.login('user@example.com', 'password123'); + await page.waitForURL('/en'); + + await use(page); + }, +}); + +test.describe('Theme Toggle', () => { + test('cycles through light β†’ dark β†’ rainbow β†’ light', async ({ authedPage }) => { + const toggle = authedPage.getByTestId('theme-toggle'); + const html = authedPage.locator('html'); + + await expect(toggle).toBeVisible(); + await expect(html).toHaveAttribute('class', /light/); + await expect(toggle).toHaveText('🌞'); + + await toggle.click(); + await expect(html).toHaveAttribute('class', /dark/); + await expect(toggle).toHaveText('πŸŒ™'); + expect(await authedPage.evaluate(() => localStorage.getItem('theme'))).toBe('dark'); + + await toggle.click(); + await expect(html).toHaveAttribute('class', /rainbow/); + await expect(toggle).toHaveText('🌈'); + expect(await authedPage.evaluate(() => localStorage.getItem('theme'))).toBe('rainbow'); + + await toggle.click(); + await expect(html).toHaveAttribute('class', /light/); + await expect(toggle).toHaveText('🌞'); + expect(await authedPage.evaluate(() => localStorage.getItem('theme'))).toBe('light'); + }); + + test('persists selected theme after reload', async ({ authedPage }) => { + const toggle = authedPage.getByTestId('theme-toggle'); + const html = authedPage.locator('html'); + + await toggle.click(); // light -> dark + await expect(html).toHaveAttribute('class', /dark/); + + await authedPage.reload(); + + await expect(html).toHaveAttribute('class', /dark/); + await expect(toggle).toHaveText('πŸŒ™'); + expect(await authedPage.evaluate(() => localStorage.getItem('theme'))).toBe('dark'); + }); + + test('captures screenshots for each theme state', async ({ authedPage }) => { + const toggle = authedPage.getByTestId('theme-toggle'); + + await expect(toggle).toHaveScreenshot('theme-toggle-light.png'); + + await toggle.click(); // light -> dark + await expect(toggle).toHaveScreenshot('theme-toggle-dark.png'); + + await toggle.click(); // dark -> rainbow + await expect(toggle).toHaveScreenshot('theme-toggle-rainbow.png'); + }); +}); diff --git a/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts-snapshots/theme-toggle-dark-chromium-linux.png b/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts-snapshots/theme-toggle-dark-chromium-linux.png new file mode 100644 index 00000000..20960986 Binary files /dev/null and b/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts-snapshots/theme-toggle-dark-chromium-linux.png differ diff --git a/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts-snapshots/theme-toggle-light-chromium-linux.png b/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts-snapshots/theme-toggle-light-chromium-linux.png new file mode 100644 index 00000000..ea3998da Binary files /dev/null and b/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts-snapshots/theme-toggle-light-chromium-linux.png differ diff --git a/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts-snapshots/theme-toggle-rainbow-chromium-linux.png b/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts-snapshots/theme-toggle-rainbow-chromium-linux.png new file mode 100644 index 00000000..a514165e Binary files /dev/null and b/apps/frontend-e2e/tests/theme-toggle.e2e.spec.ts-snapshots/theme-toggle-rainbow-chromium-linux.png differ diff --git a/apps/frontend-e2e/tsconfig.json b/apps/frontend-e2e/tsconfig.json new file mode 100644 index 00000000..cf435814 --- /dev/null +++ b/apps/frontend-e2e/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "moduleResolution": "bundler" + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/apps/frontend/src/app/[locale]/(protected)/_components/HomePage/HomePage.tsx b/apps/frontend/src/app/[locale]/(protected)/_components/HomePage/HomePage.tsx index 82511295..6a5706c7 100644 --- a/apps/frontend/src/app/[locale]/(protected)/_components/HomePage/HomePage.tsx +++ b/apps/frontend/src/app/[locale]/(protected)/_components/HomePage/HomePage.tsx @@ -12,7 +12,7 @@ export const HomePage = async () => { return (
-
+
Infinum logo
diff --git a/apps/frontend/src/app/_components/ThemeToggle/ThemeToggle.tsx b/apps/frontend/src/app/_components/ThemeToggle/ThemeToggle.tsx index d5cf659f..2c0c5eda 100644 --- a/apps/frontend/src/app/_components/ThemeToggle/ThemeToggle.tsx +++ b/apps/frontend/src/app/_components/ThemeToggle/ThemeToggle.tsx @@ -57,7 +57,7 @@ export const ThemeToggle = ({ children, ...props }: React.ButtonHTMLAttributes - diff --git a/apps/frontend/src/lib/auth/auth-options.ts b/apps/frontend/src/lib/auth/auth-options.ts index e6b225b3..74ba795b 100644 --- a/apps/frontend/src/lib/auth/auth-options.ts +++ b/apps/frontend/src/lib/auth/auth-options.ts @@ -13,6 +13,10 @@ const findUserByEmail = (_email: string) => { return MOCK_USER; }; const verifyPassword = (_password: string, _hashedPassword: string) => { + if (_password === 'invalid') { + return false; + } + return true; }; diff --git a/documentation/E2E Testing.md b/documentation/E2E Testing.md new file mode 100644 index 00000000..41a26342 --- /dev/null +++ b/documentation/E2E Testing.md @@ -0,0 +1,49 @@ +# E2E Testing (Playwright) + +## How to run + +- All E2E (root script): `pnpm e2e` + - Headed: `pnpm e2e -- --headed` + - UI runner: `pnpm e2e -- --ui` + - Debug/inspector: `pnpm e2e -- --debug` +- Update visual baselines: `pnpm e2e:update` +- Install browsers (once per machine/CI image): `pnpm e2e:install` +- Show last HTML report (if generated): `pnpm e2e:report` + +All commands can be run only for a specific package with Turbo filtering, for example `pnpm e2e --filter *frontend* -- --ui`. + +## What gets produced (gitignored) + +- `playwright-report/` β€” HTML report output when using `--reporter=html` or `show-report`. +- `reports/` β€” project-owned artifacts: visual baselines under `reports/screenshots/`, a11y reports under `reports/a11y/`. +- `test-results/` β€” per-run output (actual/expected screenshots, traces, error context); safe to delete. + Snapshots stored in `*.spec.ts-snapshots/` stay committed as baselines. + +## Monorepo layout + +- Tests: `apps/frontend-e2e/tests` +- Page objects: `apps/frontend-e2e/pages` +- Shared config: `@infinum/configs/playwright/base` +- Shared helpers: `@infinum/e2e-utils` + +## Adding E2E for a new app + +- Create a sibling E2E app `apps/-e2e` for each new app. +- Point `playwright.config.ts` to the shared base: `@infinum/configs/playwright/base`. +- Reuse helpers from `@infinum/e2e-utils` (fixtures, waits, viewports, reports). +- Keep snapshots and app-specific page objects inside that E2E app. +- Give each new Next app a unique prod port when running `next start` (e.g. `-p 3001`) so it doesn’t clash with existing frontend apps. +- Add `E2E_BASE_URL` for the new `apps/-e2e` package, ideally inline before the command in `package.json`, e.g. `"e2e": "E2E_BASE_URL=http://localhost:3001 playwright test --reporter=html",` + +## Consistency tips + +- Generate and validate snapshots in headless for stable rendering; if you must use headed, regenerate and stay consistent. +- Ensure `E2E_BASE_URL` is set when not using `http://localhost:3000`. + +## Testing with Act + +Run locally with [GitHub Act](https://github.com/nektos/act) from the repo root; ensure the apps default ports are free before starting. + +```sh +act pull_request -W ./.github/workflows/e2e.yml -j e2e +``` diff --git a/package.json b/package.json index 4345597e..10d4207c 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,16 @@ "check-licenses:root": "node scripts/check-licenses-workspace.js", "dev": "turbo run dev --parallel --ui=tui --continue", "docker:prod": "docker compose -f ./docker/docker-compose.yml", - "prepare": "husky", + "e2e": "turbo run e2e --parallel --log-order=grouped --continue", + "e2e:install": "turbo run e2e:install", + "e2e:report": "turbo run e2e:report --parallel --log-order=grouped", + "e2e:update": "pnpm e2e -- --update-snapshots", "lint": "turbo run lint lint:root --parallel --log-order=grouped --continue", "lint:root": "eslint . --cache --ignore-pattern '{apps,packages}/**'", "lint:fix": "turbo run lint:fix lint:fix:root --parallel --log-order=grouped", "lint:fix:root": "eslint . --fix --ignore-pattern '{apps,packages}/**'", "postinstall": "pnpm run bootstrap", + "prepare": "husky", "pre-commit": "lint-staged --allow-empty", "pre-push": "pnpm lint && pnpm prettier:check && pnpm test:coverage", "prettier:check": "turbo run prettier:check prettier:check:root --parallel --log-order=grouped --continue", @@ -38,6 +42,7 @@ "@eslint/js": "catalog:", "@infinum/configs": "workspace:*", "@next/eslint-plugin-next": "catalog:", + "@playwright/test": "catalog:", "eslint": "catalog:", "husky": "catalog:", "license-checker-rseidelsohn": "catalog:", diff --git a/packages/configs/package.json b/packages/configs/package.json index f0024a03..a17debdf 100644 --- a/packages/configs/package.json +++ b/packages/configs/package.json @@ -10,7 +10,12 @@ "./eslint/react": "./src/eslint-config/react.mjs", "./eslint/storybook": "./src/eslint-config/storybook.mjs", "./eslint/typescript": "./src/eslint-config/typescript.mjs", - "./jest": "./src/jest-config/index.js" + "./eslint/playwright": "./src/eslint-config/playwright.mjs", + "./jest": "./src/jest-config/index.js", + "./playwright/base": { + "types": "./src/playwright-config/base.d.ts", + "default": "./src/playwright-config/base.js" + } }, "scripts": { "check-licenses": "node ../../scripts/check-licenses-workspace.js", @@ -24,10 +29,12 @@ "@eslint/compat": "catalog:", "@eslint/js": "catalog:", "@next/eslint-plugin-next": "catalog:", + "@playwright/test": "catalog:", "@typescript-eslint/eslint-plugin": "catalog:", "eslint": "catalog:", "eslint-config-prettier": "catalog:", "eslint-plugin-jest": "catalog:", + "eslint-plugin-playwright": "catalog:", "eslint-plugin-prettier": "catalog:", "eslint-plugin-react": "catalog:", "eslint-plugin-react-hooks": "catalog:", diff --git a/packages/configs/src/eslint-config/base.mjs b/packages/configs/src/eslint-config/base.mjs index 82e372e7..6de39590 100644 --- a/packages/configs/src/eslint-config/base.mjs +++ b/packages/configs/src/eslint-config/base.mjs @@ -22,6 +22,15 @@ export default [ ...pluginJs.configs.recommended, }, { - ignores: ['**/node_modules', '**/dist', '**/build', '**/.turbo', '**/storybook-static'], + ignores: [ + '**/node_modules', + '**/dist', + '**/build', + '**/.turbo', + '**/storybook-static', + '**/.next', + '**/coverage', + 'public/**', + ], }, ]; diff --git a/packages/configs/src/eslint-config/playwright.mjs b/packages/configs/src/eslint-config/playwright.mjs new file mode 100644 index 00000000..1fac3bed --- /dev/null +++ b/packages/configs/src/eslint-config/playwright.mjs @@ -0,0 +1,8 @@ +import pluginPlaywright from 'eslint-plugin-playwright'; + +export default [ + { + files: ['**/*.e2e.{spec,test}.{js,mjs,cjs,ts,tsx}'], + ...pluginPlaywright.configs['flat/recommended'], + }, +]; diff --git a/packages/configs/src/playwright-config/base.d.ts b/packages/configs/src/playwright-config/base.d.ts new file mode 100644 index 00000000..ac9aaf92 --- /dev/null +++ b/packages/configs/src/playwright-config/base.d.ts @@ -0,0 +1,4 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; + +declare const baseConfig: PlaywrightTestConfig; +export default baseConfig; diff --git a/packages/configs/src/playwright-config/base.js b/packages/configs/src/playwright-config/base.js new file mode 100644 index 00000000..05150010 --- /dev/null +++ b/packages/configs/src/playwright-config/base.js @@ -0,0 +1,17 @@ +const { defineConfig } = require('@playwright/test'); + +/** @type {import('@playwright/test').PlaywrightTestConfig} */ +const baseConfig = { + /* Shared settings for all projects */ + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? [['github'], ['line']] : 'list', + use: { + trace: 'on-first-retry', + viewport: { width: 1280, height: 720 }, + }, +}; + +module.exports = defineConfig(baseConfig); diff --git a/packages/e2e-utils/.prettierignore b/packages/e2e-utils/.prettierignore new file mode 100644 index 00000000..01a18cf3 --- /dev/null +++ b/packages/e2e-utils/.prettierignore @@ -0,0 +1,5 @@ +# Dependencies +node_modules/ + +# Cache +.turbo/ diff --git a/packages/e2e-utils/.prettierrc.js b/packages/e2e-utils/.prettierrc.js new file mode 100644 index 00000000..c87f92a9 --- /dev/null +++ b/packages/e2e-utils/.prettierrc.js @@ -0,0 +1,6 @@ +const baseConfig = require('@infinum/configs/prettier'); + +/** @type {import('prettier').Config} */ +module.exports = { + ...baseConfig, +}; diff --git a/packages/e2e-utils/CHANGELOG.md b/packages/e2e-utils/CHANGELOG.md new file mode 100644 index 00000000..e5b6ff62 --- /dev/null +++ b/packages/e2e-utils/CHANGELOG.md @@ -0,0 +1 @@ +# Infinum E2E Utils - Changelog diff --git a/packages/e2e-utils/eslint.config.mjs b/packages/e2e-utils/eslint.config.mjs new file mode 100644 index 00000000..c2c165c4 --- /dev/null +++ b/packages/e2e-utils/eslint.config.mjs @@ -0,0 +1,18 @@ +import baseConfig from '@infinum/configs/eslint/base'; +import playwrightConfig from '@infinum/configs/eslint/playwright'; +import typescriptConfig from '@infinum/configs/eslint/typescript'; + +export default [ + ...baseConfig, + ...typescriptConfig, + ...playwrightConfig, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages/e2e-utils/package.json b/packages/e2e-utils/package.json new file mode 100644 index 00000000..c203db35 --- /dev/null +++ b/packages/e2e-utils/package.json @@ -0,0 +1,36 @@ +{ + "name": "@infinum/e2e-utils", + "version": "0.0.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts", + "files": [ + "src" + ], + "exports": { + ".": "./src/index.ts", + "./accessibility": "./src/accessibility/index.ts", + "./pages": "./src/pages/index.ts", + "./devices": "./src/devices/index.ts", + "./reports": "./src/reports/index.ts", + "./utils": "./src/utils/index.ts" + }, + "scripts": { + "check-licenses": "node ../../scripts/check-licenses-workspace.js", + "clean": "rm -rf node_modules .turbo .eslintcache", + "lint": "eslint . --cache", + "lint:fix": "eslint . --cache --fix", + "prettier:check": "prettier --check .", + "prettier:fix": "prettier --write ." + }, + "peerDependencies": { + "@playwright/test": "catalog:" + }, + "devDependencies": { + "@infinum/configs": "workspace:*", + "@types/node": "catalog:", + "eslint": "catalog:", + "prettier": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/e2e-utils/src/accessibility/index.ts b/packages/e2e-utils/src/accessibility/index.ts new file mode 100644 index 00000000..c44791fc --- /dev/null +++ b/packages/e2e-utils/src/accessibility/index.ts @@ -0,0 +1,2 @@ +export { saveHtmlReport } from './reporter'; +export { a11yRules, type A11yRule } from './rules'; diff --git a/packages/e2e-utils/src/accessibility/reporter.ts b/packages/e2e-utils/src/accessibility/reporter.ts new file mode 100644 index 00000000..0d692320 --- /dev/null +++ b/packages/e2e-utils/src/accessibility/reporter.ts @@ -0,0 +1,26 @@ +import fs from 'fs'; +import path from 'path'; + +/** + * Save HTML report to specified directory. + * @param axeReportContent - HTML content created by axe-html-reporter + * @param currentBrowser - name of the current browser + * @param reportName - name of the report file + * @param reportsDir - base directory for reports (default: 'reports/a11y') + */ +export function saveHtmlReport( + axeReportContent: string, + currentBrowser: string, + reportName: string, + reportsDir = 'reports/a11y' +): void { + const reportPath = path.join(reportsDir, currentBrowser, reportName); + const reportDir = path.dirname(reportPath); + + if (!fs.existsSync(reportDir)) { + fs.mkdirSync(reportDir, { recursive: true }); + } + + fs.writeFileSync(reportPath, axeReportContent); + console.info(`HTML report created: ${reportPath}`); +} diff --git a/packages/e2e-utils/src/accessibility/rules.ts b/packages/e2e-utils/src/accessibility/rules.ts new file mode 100644 index 00000000..79d15970 --- /dev/null +++ b/packages/e2e-utils/src/accessibility/rules.ts @@ -0,0 +1,8 @@ +/** + * Array of accessibility rules to check for violations. + * Add / Remove rules to customize the accessibility scan scope. + * @see https://playwright.dev/docs/accessibility-testing#scanning-for-wcag-violations + */ +export const a11yRules: string[] = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa']; + +export type A11yRule = (typeof a11yRules)[number]; diff --git a/packages/e2e-utils/src/devices/index.ts b/packages/e2e-utils/src/devices/index.ts new file mode 100644 index 00000000..5a3e35bc --- /dev/null +++ b/packages/e2e-utils/src/devices/index.ts @@ -0,0 +1 @@ +export { viewports, type ViewportPreset } from './viewports'; diff --git a/packages/e2e-utils/src/devices/viewports.ts b/packages/e2e-utils/src/devices/viewports.ts new file mode 100644 index 00000000..a3aefb91 --- /dev/null +++ b/packages/e2e-utils/src/devices/viewports.ts @@ -0,0 +1,13 @@ +import { ViewportSize } from '@playwright/test'; + +/** + * Common viewport presets for different device types. + */ +export const viewports = { + mobile: { width: 375, height: 812 } as ViewportSize, + tablet: { width: 768, height: 1024 } as ViewportSize, + desktop: { width: 1280, height: 720 } as ViewportSize, + 'desktop-large': { width: 1920, height: 1080 } as ViewportSize, +} as const; + +export type ViewportPreset = keyof typeof viewports; diff --git a/packages/e2e-utils/src/index.ts b/packages/e2e-utils/src/index.ts new file mode 100644 index 00000000..1668906b --- /dev/null +++ b/packages/e2e-utils/src/index.ts @@ -0,0 +1,14 @@ +// Accessibility utilities +export * from './accessibility'; + +// Page Object Model utilities +export * from './pages'; + +// Device/viewport utilities +export * from './devices'; + +// Report utilities +export * from './reports'; + +// General utilities +export * from './utils'; diff --git a/packages/e2e-utils/src/pages/base-page.ts b/packages/e2e-utils/src/pages/base-page.ts new file mode 100644 index 00000000..3e8029d7 --- /dev/null +++ b/packages/e2e-utils/src/pages/base-page.ts @@ -0,0 +1,75 @@ +import { Page, Locator } from '@playwright/test'; +import { getBaseUrl } from '../utils/config'; + +/** + * Options for BasePage constructor. + */ +export interface BasePageOptions { + /** + * Base URL for the application. If not provided, will use E2E_BASE_URL env var or default. + */ + baseURL?: string; +} + +/** + * Abstract base class for Page Object Models. + * Provides common functionality for all page objects. + */ +export abstract class BasePage { + readonly page: Page; + protected readonly baseURL: string; + + constructor(page: Page, options?: BasePageOptions) { + this.page = page; + this.baseURL = options?.baseURL ?? getBaseUrl(); + } + + /** + * Navigate to the page URL. + * Override this method in subclasses to set the specific URL. + */ + abstract goto(): Promise; + + /** + * Navigate to a URL (relative or absolute). + * Relative URLs will be resolved against the base URL. + * @param url - URL to navigate to + * @param options - Navigation options + */ + protected async navigateTo( + url: string, + options?: { waitUntil?: 'load' | 'domcontentloaded' | 'networkidle'; timeout?: number } + ): Promise { + const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url.startsWith('/') ? url : `/${url}`}`; + await this.page.goto(fullUrl, { + waitUntil: options?.waitUntil ?? 'networkidle', + timeout: options?.timeout ?? 30000, + }); + } + + /** + * Wait for the page to be fully loaded. + * @param options - Wait options + */ + protected async waitForLoad(options?: { timeout?: number }): Promise { + await this.page.waitForLoadState('networkidle', { timeout: options?.timeout ?? 15000 }); + } + + /** + * Wait for a specific element to be visible. + * @param locator - Element locator + * @param options - Wait options + */ + protected async waitForVisible(locator: Locator, options?: { timeout?: number }): Promise { + await locator.waitFor({ state: 'visible', timeout: options?.timeout ?? 10000 }); + } + + /** + * Wait for navigation to complete. + * Alias for waitForLoad for semantic clarity. + * @param options - Wait options + */ + protected async waitForNavigation(options?: { timeout?: number }): Promise { + await this.waitForLoad(options); + } +} diff --git a/packages/e2e-utils/src/pages/index.ts b/packages/e2e-utils/src/pages/index.ts new file mode 100644 index 00000000..cef5d560 --- /dev/null +++ b/packages/e2e-utils/src/pages/index.ts @@ -0,0 +1,2 @@ +export { BasePage } from './base-page'; +export type { BasePageOptions } from './base-page'; diff --git a/packages/e2e-utils/src/reports/attachments.ts b/packages/e2e-utils/src/reports/attachments.ts new file mode 100644 index 00000000..7b99b29c --- /dev/null +++ b/packages/e2e-utils/src/reports/attachments.ts @@ -0,0 +1,27 @@ +import { TestInfo } from '@playwright/test'; + +/** + * Attach a screenshot to the test report. + * @param testInfo - TestInfo from Playwright test + * @param screenshot - Screenshot buffer + * @param name - Name for the attachment + */ +export async function attachScreenshot(testInfo: TestInfo, screenshot: Buffer, name: string): Promise { + await testInfo.attach(name, { + body: screenshot, + contentType: 'image/png', + }); +} + +/** + * Attach JSON data to the test report. + * @param testInfo - TestInfo from Playwright test + * @param data - Data to attach + * @param name - Name for the attachment + */ +export async function attachJson(testInfo: TestInfo, data: unknown, name: string): Promise { + await testInfo.attach(name, { + body: JSON.stringify(data, null, 2), + contentType: 'application/json', + }); +} diff --git a/packages/e2e-utils/src/reports/directories.ts b/packages/e2e-utils/src/reports/directories.ts new file mode 100644 index 00000000..247810a5 --- /dev/null +++ b/packages/e2e-utils/src/reports/directories.ts @@ -0,0 +1,17 @@ +import path from 'path'; + +/** + * Get screenshot path with consistent naming. + * @param browserName - Name of the browser + * @param testName - Name of the test + * @param extension - File extension (default: 'png') + * @param baseDir - Base directory (default: 'reports/screenshots') + */ +export function getScreenshotPath( + browserName: string, + testName: string, + extension = 'png', + baseDir = 'reports/screenshots' +): string { + return path.join(baseDir, `${browserName}-${testName}.${extension}`); +} diff --git a/packages/e2e-utils/src/reports/index.ts b/packages/e2e-utils/src/reports/index.ts new file mode 100644 index 00000000..f5d0740c --- /dev/null +++ b/packages/e2e-utils/src/reports/index.ts @@ -0,0 +1,2 @@ +export { getScreenshotPath } from './directories'; +export { attachScreenshot, attachJson } from './attachments'; diff --git a/packages/e2e-utils/src/utils/config.ts b/packages/e2e-utils/src/utils/config.ts new file mode 100644 index 00000000..4060ba3f --- /dev/null +++ b/packages/e2e-utils/src/utils/config.ts @@ -0,0 +1,7 @@ +/** + * Get base URL from environment or default. + * @param defaultUrl - Default URL (default: 'http://localhost:3000') + */ +export function getBaseUrl(defaultUrl = 'http://localhost:3000'): string { + return process.env.E2E_BASE_URL ?? defaultUrl; +} diff --git a/packages/e2e-utils/src/utils/index.ts b/packages/e2e-utils/src/utils/index.ts new file mode 100644 index 00000000..e8f4d136 --- /dev/null +++ b/packages/e2e-utils/src/utils/index.ts @@ -0,0 +1,3 @@ +export { waitForUrl } from './waits'; +export { gotoWithRetry, type GotoWithRetryOptions } from './navigation'; +export { getBaseUrl } from './config'; diff --git a/packages/e2e-utils/src/utils/navigation.ts b/packages/e2e-utils/src/utils/navigation.ts new file mode 100644 index 00000000..f30d7edf --- /dev/null +++ b/packages/e2e-utils/src/utils/navigation.ts @@ -0,0 +1,40 @@ +import { Page } from '@playwright/test'; + +export interface GotoWithRetryOptions { + waitUntil?: 'load' | 'domcontentloaded' | 'networkidle'; + timeout?: number; + maxRetries?: number; + baseURL?: string; +} + +/** + * Navigate to URL with retry logic and exponential backoff. + * @param page - Playwright page + * @param url - URL to navigate to (relative or absolute) + * @param options - Navigation options + */ +export async function gotoWithRetry(page: Page, url: string, options?: GotoWithRetryOptions): Promise { + const maxRetries = options?.maxRetries ?? 3; + const baseURL = options?.baseURL; + const fullUrl = baseURL && !url.startsWith('http') ? `${baseURL}${url}` : url; + let lastError: Error | undefined; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + await page.goto(fullUrl, { + waitUntil: options?.waitUntil ?? 'networkidle', + timeout: options?.timeout ?? 30000, + }); + return; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + if (attempt < maxRetries) { + // Exponential backoff: 1s, 2s, 4s, etc. + const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + + throw lastError ?? new Error(`Navigation to ${fullUrl} failed after ${maxRetries} retries`); +} diff --git a/packages/e2e-utils/src/utils/waits.ts b/packages/e2e-utils/src/utils/waits.ts new file mode 100644 index 00000000..3aacff09 --- /dev/null +++ b/packages/e2e-utils/src/utils/waits.ts @@ -0,0 +1,11 @@ +import { Page } from '@playwright/test'; + +/** + * Wait for URL to match pattern. + * @param page - Playwright page + * @param url - URL pattern or string + * @param timeout - Timeout in milliseconds (default: 10000) + */ +export async function waitForUrl(page: Page, url: string | RegExp, timeout = 10000): Promise { + await page.waitForURL(url, { timeout }); +} diff --git a/packages/e2e-utils/tsconfig.json b/packages/e2e-utils/tsconfig.json new file mode 100644 index 00000000..b0dbe5c6 --- /dev/null +++ b/packages/e2e-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "outDir": "./dist" + }, + "include": [ + "src/index.ts", + "src/accessibility/**/*.ts", + "src/devices/**/*.ts", + "src/pages/**/*.ts", + "src/reports/**/*.ts", + "src/utils/**/*.ts" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4dfa8bd..9872b9f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,9 @@ catalogs: '@actions/core': specifier: 1.11.1 version: 1.11.1 + '@axe-core/playwright': + specifier: 4.10.2 + version: 4.10.2 '@changesets/cli': specifier: 2.29.7 version: 2.29.7 @@ -27,6 +30,9 @@ catalogs: '@next/eslint-plugin-next': specifier: 16.0.1 version: 16.0.1 + '@playwright/test': + specifier: 1.55.0 + version: 1.55.0 '@radix-ui/react-label': specifier: 2.1.7 version: 2.1.7 @@ -87,6 +93,12 @@ catalogs: autoprefixer: specifier: 10.4.21 version: 10.4.21 + axe-core: + specifier: 4.10.3 + version: 4.10.3 + axe-html-reporter: + specifier: 2.2.11 + version: 2.2.11 class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -105,6 +117,9 @@ catalogs: eslint-plugin-jest: specifier: 29.0.1 version: 29.0.1 + eslint-plugin-playwright: + specifier: 2.3.0 + version: 2.3.0 eslint-plugin-prettier: specifier: 5.5.4 version: 5.5.4 @@ -231,6 +246,9 @@ importers: '@next/eslint-plugin-next': specifier: 'catalog:' version: 16.0.1 + '@playwright/test': + specifier: 'catalog:' + version: 1.55.0 eslint: specifier: 'catalog:' version: 9.39.0(jiti@2.6.1) @@ -278,16 +296,16 @@ importers: version: 2.0.3 next: specifier: 'catalog:' - version: 16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) next-auth: specifier: 'catalog:' - version: 4.24.13(next@16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 4.24.13(next@16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) next-intl: specifier: 'catalog:' - version: 4.4.0(next@16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(typescript@5.8.3) + version: 4.4.0(next@16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(typescript@5.8.3) next-public-env: specifier: 1.0.0 - version: 1.0.0(next@16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(zod@3.25.76) + version: 1.0.0(next@16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(zod@3.25.76) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -362,6 +380,39 @@ importers: specifier: 'catalog:' version: 5.8.3 + apps/frontend-e2e: + devDependencies: + '@axe-core/playwright': + specifier: 'catalog:' + version: 4.10.2(playwright-core@1.55.0) + '@infinum/configs': + specifier: workspace:* + version: link:../../packages/configs + '@infinum/e2e-utils': + specifier: workspace:* + version: link:../../packages/e2e-utils + '@playwright/test': + specifier: 'catalog:' + version: 1.55.0 + '@types/node': + specifier: 'catalog:' + version: 24.10.0 + axe-core: + specifier: 'catalog:' + version: 4.10.3 + axe-html-reporter: + specifier: 'catalog:' + version: 2.2.11(axe-core@4.10.3) + eslint: + specifier: 'catalog:' + version: 9.39.0(jiti@2.6.1) + prettier: + specifier: 'catalog:' + version: 3.6.2 + typescript: + specifier: 'catalog:' + version: 5.8.3 + apps/storybook: dependencies: '@infinum/ui': @@ -394,7 +445,7 @@ importers: version: 10.1.4(storybook@10.1.4(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/nextjs': specifier: 'catalog:' - version: 10.1.4(esbuild@0.25.8)(msw@2.10.4(@types/node@24.10.0)(typescript@5.8.3))(next@16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.4(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.100.2(esbuild@0.25.8)) + version: 10.1.4(esbuild@0.25.8)(msw@2.10.4(@types/node@24.10.0)(typescript@5.8.3))(next@16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.4(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.100.2(esbuild@0.25.8)) '@tailwindcss/postcss': specifier: 'catalog:' version: 4.1.16 @@ -431,6 +482,9 @@ importers: '@next/eslint-plugin-next': specifier: 'catalog:' version: 16.0.1 + '@playwright/test': + specifier: 'catalog:' + version: 1.55.0 '@typescript-eslint/eslint-plugin': specifier: 'catalog:' version: 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3) @@ -443,6 +497,9 @@ importers: eslint-plugin-jest: specifier: 'catalog:' version: 29.0.1(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.0(jiti@2.6.1))(jest@30.2.0(@types/node@24.10.0)(esbuild-register@3.6.0(esbuild@0.25.8)))(typescript@5.8.3) + eslint-plugin-playwright: + specifier: 'catalog:' + version: 2.3.0(eslint@9.39.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: 'catalog:' version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1))(prettier@3.6.2) @@ -474,6 +531,28 @@ importers: specifier: 'catalog:' version: 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3) + packages/e2e-utils: + dependencies: + '@playwright/test': + specifier: 'catalog:' + version: 1.55.0 + devDependencies: + '@infinum/configs': + specifier: workspace:* + version: link:../configs + '@types/node': + specifier: 'catalog:' + version: 24.10.0 + eslint: + specifier: 'catalog:' + version: 9.39.0(jiti@2.6.1) + prettier: + specifier: 'catalog:' + version: 3.6.2 + typescript: + specifier: 'catalog:' + version: 5.8.3 + packages/ui: dependencies: '@radix-ui/react-label': @@ -509,7 +588,7 @@ importers: version: link:../configs '@storybook/nextjs': specifier: 'catalog:' - version: 10.1.4(esbuild@0.25.8)(msw@2.10.4(@types/node@24.10.0)(typescript@5.8.3))(next@16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.4(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.100.2(esbuild@0.25.8)) + version: 10.1.4(esbuild@0.25.8)(msw@2.10.4(@types/node@24.10.0)(typescript@5.8.3))(next@16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.4(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.100.2(esbuild@0.25.8)) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 @@ -612,6 +691,11 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@axe-core/playwright@4.10.2': + resolution: {integrity: sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1526,12 +1610,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2097,6 +2175,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.55.0': + resolution: {integrity: sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==} + engines: {node: '>=18'} + hasBin: true + '@pmmmwh/react-refresh-webpack-plugin@0.5.17': resolution: {integrity: sha512-tXDyE1/jzFsHXjhRZQ3hMl0IVhYe5qula43LDWIhVfjp9G/nT5OQY5AORVOrkEGAUltBJOfOWeETbmhm6kHhuQ==} engines: {node: '>= 10.13'} @@ -2784,9 +2867,6 @@ packages: '@types/node@22.16.5': resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==} - '@types/node@24.1.0': - resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==} - '@types/node@24.10.0': resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} @@ -2846,32 +2926,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.38.0': - resolution: {integrity: sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/project-service@8.46.2': resolution: {integrity: sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.38.0': - resolution: {integrity: sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.46.2': resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.38.0': - resolution: {integrity: sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/tsconfig-utils@8.46.2': resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2885,33 +2949,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.38.0': - resolution: {integrity: sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.46.2': resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.38.0': - resolution: {integrity: sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/typescript-estree@8.46.2': resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.38.0': - resolution: {integrity: sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.46.2': resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2919,10 +2966,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.38.0': - resolution: {integrity: sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.46.2': resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3318,6 +3361,12 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} + axe-html-reporter@2.2.11: + resolution: {integrity: sha512-WlF+xlNVgNVWiM6IdVrsh+N0Cw7qupe5HT9N6Uyi+aN7f6SSi92RDomiP1noW8OWIV85V6x404m5oKMeqRV3tQ==} + engines: {node: '>=8.9.0'} + peerDependencies: + axe-core: '>=3' + babel-jest@30.2.0: resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -4104,6 +4153,12 @@ packages: jest: optional: true + eslint-plugin-playwright@2.3.0: + resolution: {integrity: sha512-7UeUuIb5SZrNkrUGb2F+iwHM97kn33/huajcVtAaQFCSMUYGNFvjzRPil5C0OIppslPfuOV68M/zsisXx+/ZvQ==} + engines: {node: '>=16.9.0'} + peerDependencies: + eslint: '>=8.40.0' + eslint-plugin-prettier@5.5.4: resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4401,6 +4456,11 @@ packages: fs-monkey@1.1.0: resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -5600,6 +5660,10 @@ packages: typescript: optional: true + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -6023,6 +6087,16 @@ packages: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} + playwright-core@1.55.0: + resolution: {integrity: sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.55.0: + resolution: {integrity: sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -7193,9 +7267,6 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici-types@7.8.0: - resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} - undici@5.29.0: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} @@ -7634,6 +7705,11 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 + '@axe-core/playwright@4.10.2(playwright-core@1.55.0)': + dependencies: + axe-core: 4.10.3 + playwright-core: 1.55.0 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -8748,11 +8824,6 @@ snapshots: '@esbuild/win32-x64@0.25.8': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.39.0(jiti@2.6.1))': - dependencies: - eslint: 9.39.0(jiti@2.6.1) - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.0(jiti@2.6.1))': dependencies: eslint: 9.39.0(jiti@2.6.1) @@ -9432,6 +9503,10 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.55.0': + dependencies: + playwright: 1.55.0 + '@pmmmwh/react-refresh-webpack-plugin@0.5.17(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.100.2(esbuild@0.25.8))': dependencies: ansi-html: 0.0.9 @@ -9811,7 +9886,7 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - '@storybook/nextjs@10.1.4(esbuild@0.25.8)(msw@2.10.4(@types/node@24.10.0)(typescript@5.8.3))(next@16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.4(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.100.2(esbuild@0.25.8))': + '@storybook/nextjs@10.1.4(esbuild@0.25.8)(msw@2.10.4(@types/node@24.10.0)(typescript@5.8.3))(next@16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.4(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.100.2(esbuild@0.25.8))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.0) @@ -9835,7 +9910,7 @@ snapshots: css-loader: 6.11.0(webpack@5.100.2(esbuild@0.25.8)) image-size: 2.0.2 loader-utils: 3.3.1 - next: 16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) node-polyfill-webpack-plugin: 2.0.1(webpack@5.100.2(esbuild@0.25.8)) postcss: 8.5.6 postcss-loader: 8.1.1(postcss@8.5.6)(typescript@5.8.3)(webpack@5.100.2(esbuild@0.25.8)) @@ -10084,7 +10159,7 @@ snapshots: '@types/concat-stream@2.0.3': dependencies: - '@types/node': 24.1.0 + '@types/node': 24.10.0 '@types/cookie@0.6.0': {} @@ -10149,10 +10224,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@24.1.0': - dependencies: - undici-types: 7.8.0 - '@types/node@24.10.0': dependencies: undici-types: 7.16.0 @@ -10218,15 +10289,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.38.0(typescript@5.8.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) - '@typescript-eslint/types': 8.38.0 - debug: 4.4.1 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/project-service@8.46.2(typescript@5.8.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.8.3) @@ -10236,20 +10298,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.38.0': - dependencies: - '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/visitor-keys': 8.38.0 - '@typescript-eslint/scope-manager@8.46.2': dependencies: '@typescript-eslint/types': 8.46.2 '@typescript-eslint/visitor-keys': 8.46.2 - '@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.8.3)': - dependencies: - typescript: 5.8.3 - '@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.8.3)': dependencies: typescript: 5.8.3 @@ -10266,26 +10319,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.38.0': {} - '@typescript-eslint/types@8.46.2': {} - '@typescript-eslint/typescript-estree@8.38.0(typescript@5.8.3)': - dependencies: - '@typescript-eslint/project-service': 8.38.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) - '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/visitor-keys': 8.38.0 - debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.46.2(typescript@5.8.3)': dependencies: '@typescript-eslint/project-service': 8.46.2(typescript@5.8.3) @@ -10302,20 +10337,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.38.0(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3)': - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.39.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.38.0 - '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) - eslint: 9.39.0(jiti@2.6.1) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.39.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.46.2 '@typescript-eslint/types': 8.46.2 '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.8.3) @@ -10324,11 +10348,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.38.0': - dependencies: - '@typescript-eslint/types': 8.38.0 - eslint-visitor-keys: 4.2.1 - '@typescript-eslint/visitor-keys@8.46.2': dependencies: '@typescript-eslint/types': 8.46.2 @@ -10718,6 +10737,11 @@ snapshots: axe-core@4.10.3: {} + axe-html-reporter@2.2.11(axe-core@4.10.3): + dependencies: + axe-core: 4.10.3 + mustache: 4.2.0 + babel-jest@30.2.0(@babel/core@7.28.0): dependencies: '@babel/core': 7.28.0 @@ -11616,7 +11640,7 @@ snapshots: eslint-plugin-jest@29.0.1(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.0(jiti@2.6.1))(jest@30.2.0(@types/node@24.10.0)(esbuild-register@3.6.0(esbuild@0.25.8)))(typescript@5.8.3): dependencies: - '@typescript-eslint/utils': 8.38.0(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.0(jiti@2.6.1) optionalDependencies: '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.8.3) @@ -11625,6 +11649,11 @@ snapshots: - supports-color - typescript + eslint-plugin-playwright@2.3.0(eslint@9.39.0(jiti@2.6.1)): + dependencies: + eslint: 9.39.0(jiti@2.6.1) + globals: 16.5.0 + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1))(prettier@3.6.2): dependencies: eslint: 9.39.0(jiti@2.6.1) @@ -12035,6 +12064,9 @@ snapshots: fs-monkey@1.1.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -13503,6 +13535,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + mustache@4.2.0: {} + mute-stream@2.0.0: {} nano-spawn@2.0.0: {} @@ -13517,13 +13551,13 @@ snapshots: neo-async@2.6.2: {} - next-auth@4.24.13(next@16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next-auth@4.24.13(next@16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.2 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.26.9 @@ -13532,19 +13566,19 @@ snapshots: react-dom: 19.2.0(react@19.2.0) uuid: 8.3.2 - next-intl@4.4.0(next@16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(typescript@5.8.3): + next-intl@4.4.0(next@16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(typescript@5.8.3): dependencies: '@formatjs/intl-localematcher': 0.5.10 negotiator: 1.0.0 - next: 16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 use-intl: 4.4.0(react@19.2.0) optionalDependencies: typescript: 5.8.3 - next-public-env@1.0.0(next@16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(zod@3.25.76): + next-public-env@1.0.0(next@16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(zod@3.25.76): dependencies: - next: 16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 zod: 3.25.76 @@ -13553,7 +13587,7 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - next@16.0.7(@babel/core@7.28.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next@16.0.7(@babel/core@7.28.0)(@playwright/test@1.55.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@next/env': 16.0.7 '@swc/helpers': 0.5.15 @@ -13564,6 +13598,7 @@ snapshots: styled-jsx: 5.1.6(@babel/core@7.28.0)(react@19.2.0) optionalDependencies: '@next/swc-linux-x64-gnu': 16.0.7 + '@playwright/test': 1.55.0 sharp: 0.34.4 transitivePeerDependencies: - '@babel/core' @@ -13928,6 +13963,14 @@ snapshots: dependencies: find-up: 6.3.0 + playwright-core@1.55.0: {} + + playwright@1.55.0: + dependencies: + playwright-core: 1.55.0 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.8.3)(webpack@5.100.2(esbuild@0.25.8)): @@ -15203,8 +15246,6 @@ snapshots: undici-types@7.16.0: {} - undici-types@7.8.0: {} - undici@5.29.0: dependencies: '@fastify/busboy': 2.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 24159416..01f39e62 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,12 +4,14 @@ packages: catalog: '@actions/core': 1.11.1 + '@axe-core/playwright': 4.10.2 '@changesets/cli': 2.29.7 '@eslint/compat': 1.4.1 '@eslint/js': 9.39.0 '@next/bundle-analyzer': 16.0.1 '@next/env': 16.0.1 '@next/eslint-plugin-next': 16.0.1 + '@playwright/test': 1.55.0 '@radix-ui/react-label': 2.1.7 '@radix-ui/react-select': 2.2.6 '@radix-ui/react-slot': 1.2.3 @@ -30,12 +32,15 @@ catalog: '@types/react-dom': 19.2.2 '@typescript-eslint/eslint-plugin': 8.46.2 autoprefixer: 10.4.21 + axe-core: 4.10.3 + axe-html-reporter: 2.2.11 class-variance-authority: 0.7.1 clsx: 2.1.1 envsafe: 2.0.3 eslint: 9.39.0 eslint-config-prettier: 10.1.8 eslint-plugin-jest: 29.0.1 + eslint-plugin-playwright: 2.3.0 eslint-plugin-prettier: 5.5.4 eslint-plugin-react: 7.37.5 eslint-plugin-react-hooks: 7.0.1 diff --git a/scripts/check-licenses-workspace.js b/scripts/check-licenses-workspace.js index 4d081b68..3cc63c4c 100644 --- a/scripts/check-licenses-workspace.js +++ b/scripts/check-licenses-workspace.js @@ -19,7 +19,7 @@ const { execSync } = require('child_process'); // List of allowed licenses, if you want to allow more licenses, you can add them to the list // If any of the installed dependencies has a license that is not in the list, the license check will fail -const ALLOWED_LICENSES = ['MIT', 'ISC', 'BSD-2-Clause', 'BSD-3-Clause', 'Apache-2.0']; +const ALLOWED_LICENSES = ['MIT', 'ISC', 'BSD-2-Clause', 'BSD-3-Clause', 'Apache-2.0', 'MPL-2.0']; // List of dependencies that you want to ignore during the license check // If you're excluding a dependency, make sure to add a comment explaining why it's excluded, diff --git a/turbo.json b/turbo.json index d76771bc..70f913dd 100644 --- a/turbo.json +++ b/turbo.json @@ -95,6 +95,23 @@ }, "//#check-licenses:root": { "cache": false + }, + "e2e": { + "dependsOn": ["^build"], + "cache": false, + "inputs": [ + "$TURBO_DEFAULT$", + "**/*.e2e.{spec,test}.{ts,tsx,js,jsx}", + "../../packages/e2e-utils/src/**/*.{ts,js}", + "../../packages/configs/src/playwright-config/**/*.{js,ts}" + ] + }, + "e2e:report": { + "dependsOn": ["^e2e"], + "cache": false + }, + "e2e:install": { + "cache": false } } }