add end-to-end testing setup with Playwright and initial test cases
This commit is contained in:
138
.github/workflows/e2e.yml
vendored
Normal file
138
.github/workflows/e2e.yml
vendored
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'e2e/**'
|
||||||
|
- '.github/workflows/e2e.yml'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'e2e/**'
|
||||||
|
- '.github/workflows/e2e.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# ── Node / frontend ────────────────────────────────────────────────────
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
# Platform-specific native packages required on Linux
|
||||||
|
npm install --force \
|
||||||
|
@rollup/rollup-linux-x64-gnu@4.34.9 \
|
||||||
|
@swc/core-linux-x64-gnu
|
||||||
|
|
||||||
|
# ── Playwright ────────────────────────────────────────────────────────
|
||||||
|
- name: Install Playwright dependencies
|
||||||
|
run: npm ci
|
||||||
|
working-directory: e2e
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
working-directory: e2e
|
||||||
|
|
||||||
|
# ── Backend binary ────────────────────────────────────────────────────
|
||||||
|
- name: Get latest donetick release tag
|
||||||
|
id: release
|
||||||
|
run: |
|
||||||
|
TAG=$(curl -fsSL https://api.github.com/repos/donetick/donetick/releases/latest \
|
||||||
|
| grep '"tag_name"' | head -1 \
|
||||||
|
| sed 's/.*"tag_name": "\(.*\)".*/\1/')
|
||||||
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Using backend tag: $TAG"
|
||||||
|
|
||||||
|
- name: Cache donetick binary
|
||||||
|
id: cache-binary
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: .donetick-bin/
|
||||||
|
key: donetick-${{ steps.release.outputs.tag }}-linux-x86_64
|
||||||
|
|
||||||
|
- name: Download and extract donetick binary
|
||||||
|
if: steps.cache-binary.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
mkdir -p .donetick-bin
|
||||||
|
curl -fsSL \
|
||||||
|
"https://github.com/donetick/donetick/releases/download/${{ steps.release.outputs.tag }}/donetick_Linux_x86_64.tar.gz" \
|
||||||
|
-o .donetick-bin/donetick_Linux_x86_64.tar.gz
|
||||||
|
tar -xzf .donetick-bin/donetick_Linux_x86_64.tar.gz -C .donetick-bin
|
||||||
|
chmod +x .donetick-bin/donetick
|
||||||
|
|
||||||
|
- name: Write backend config
|
||||||
|
run: |
|
||||||
|
mkdir -p .donetick-bin/config
|
||||||
|
cat > .donetick-bin/config/selfhosted.yaml << 'EOF'
|
||||||
|
name: "selfhosted"
|
||||||
|
is_done_tick_dot_com: false
|
||||||
|
is_user_creation_disabled: false
|
||||||
|
database:
|
||||||
|
type: "sqlite"
|
||||||
|
migration: true
|
||||||
|
jwt:
|
||||||
|
secret: "e2e_test_secret_32chars_long_ok!"
|
||||||
|
session_time: 168h
|
||||||
|
max_refresh: 1440h
|
||||||
|
server:
|
||||||
|
port: 2021
|
||||||
|
read_timeout: 10s
|
||||||
|
write_timeout: 10s
|
||||||
|
rate_period: 60s
|
||||||
|
rate_limit: 300
|
||||||
|
cors_allow_origins:
|
||||||
|
- "http://localhost:5173"
|
||||||
|
- "http://localhost:4173"
|
||||||
|
serve_frontend: false
|
||||||
|
logging:
|
||||||
|
level: "warn"
|
||||||
|
encoding: "json"
|
||||||
|
development: false
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Start donetick backend
|
||||||
|
working-directory: .donetick-bin
|
||||||
|
env:
|
||||||
|
DT_ENV: selfhosted
|
||||||
|
DT_SQLITE_PATH: /tmp/donetick-e2e.db
|
||||||
|
run: ./donetick &
|
||||||
|
|
||||||
|
# ── Frontend ──────────────────────────────────────────────────────────
|
||||||
|
# Build with --mode development so .env.development is loaded,
|
||||||
|
# which sets VITE_APP_API_URL=http://localhost:2021
|
||||||
|
- name: Build frontend
|
||||||
|
run: npx vite build --mode development
|
||||||
|
|
||||||
|
- name: Serve frontend
|
||||||
|
run: npx vite preview --port 5173 &
|
||||||
|
|
||||||
|
# ── Tests ─────────────────────────────────────────────────────────────
|
||||||
|
- name: Run Playwright tests
|
||||||
|
working-directory: e2e
|
||||||
|
env:
|
||||||
|
E2E_FRONTEND_URL: http://localhost:5173
|
||||||
|
E2E_API_URL: http://localhost:2021
|
||||||
|
run: npx playwright test
|
||||||
|
|
||||||
|
- name: Upload test artifacts
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: |
|
||||||
|
e2e/playwright-report/
|
||||||
|
e2e/test-results/
|
||||||
|
retention-days: 7
|
||||||
4
e2e/.gitignore
vendored
Normal file
4
e2e/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
.auth/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
49
e2e/fixtures/auth.js
Normal file
49
e2e/fixtures/auth.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { test as base, expect } from '@playwright/test'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill and submit the signup form through the UI.
|
||||||
|
* After successful signup the app auto-logs in and redirects to /chores.
|
||||||
|
*/
|
||||||
|
export async function signUpViaUI(page, { username, email, password, displayName }) {
|
||||||
|
await page.goto('/signup')
|
||||||
|
await page.locator('#username').fill(username)
|
||||||
|
await page.locator('#email').fill(email)
|
||||||
|
await page.locator('#password').fill(password)
|
||||||
|
await page.locator('#displayName').fill(displayName)
|
||||||
|
await page.getByRole('button', { name: 'Sign Up' }).click()
|
||||||
|
await page.waitForURL('**/chores', { timeout: 10_000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill and submit the login form through the UI.
|
||||||
|
* After successful login the app redirects to /chores.
|
||||||
|
*/
|
||||||
|
export async function loginViaUI(page, { username, password }) {
|
||||||
|
await page.goto('/login')
|
||||||
|
// The username input on the login page has id="email" (quirk of the form)
|
||||||
|
await page.locator('#email').fill(username)
|
||||||
|
await page.locator('#password').fill(password)
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click()
|
||||||
|
await page.waitForURL('**/chores', { timeout: 10_000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Playwright test fixture that provides a page already authenticated as the
|
||||||
|
* shared E2E user (via persisted storage state, no UI interaction required).
|
||||||
|
*/
|
||||||
|
export const test = base.extend({
|
||||||
|
authenticatedPage: async ({ browser }, use) => {
|
||||||
|
const ctx = await browser.newContext({
|
||||||
|
storageState: path.join(__dirname, '..', '.auth', 'state.json'),
|
||||||
|
})
|
||||||
|
const page = await ctx.newPage()
|
||||||
|
await use(page)
|
||||||
|
await ctx.close()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export { expect }
|
||||||
96
e2e/global-setup.js
Normal file
96
e2e/global-setup.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { writeFile, mkdir } from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
// FRONTEND_URL: what the browser navigates to (Playwright baseURL)
|
||||||
|
// API_URL: where the Go API lives (may be the same in production builds)
|
||||||
|
export const FRONTEND_URL =
|
||||||
|
process.env.E2E_FRONTEND_URL ||
|
||||||
|
process.env.E2E_BASE_URL ||
|
||||||
|
'http://localhost:5173'
|
||||||
|
export const API_URL =
|
||||||
|
process.env.E2E_API_URL || process.env.E2E_BASE_URL || 'http://localhost:2021'
|
||||||
|
|
||||||
|
export const TEST_USER = {
|
||||||
|
username: 'e2e.user',
|
||||||
|
email: 'e2e@donetick.test',
|
||||||
|
password: 'E2ePassword123!',
|
||||||
|
displayName: 'E2E User',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global setup: create the shared E2E test user (idempotent) and persist
|
||||||
|
* the JWT token as a Playwright storage state so authenticated tests can
|
||||||
|
* skip the login UI.
|
||||||
|
*/
|
||||||
|
export default async function globalSetup() {
|
||||||
|
await waitForServer(API_URL)
|
||||||
|
|
||||||
|
// Create user – ignore 409/conflict so re-runs are safe
|
||||||
|
const signupRes = await fetch(`${API_URL}/api/v1/auth/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(TEST_USER),
|
||||||
|
})
|
||||||
|
if (!signupRes.ok) {
|
||||||
|
const body = await signupRes.text()
|
||||||
|
// Treat "already exists" errors as success so re-runs are idempotent
|
||||||
|
if (!body.includes('already exists') && !body.includes('already taken')) {
|
||||||
|
throw new Error(`Signup failed (${signupRes.status}): ${body}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login and grab the access token
|
||||||
|
const loginRes = await fetch(`${API_URL}/api/v1/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: TEST_USER.username,
|
||||||
|
password: TEST_USER.password,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (!loginRes.ok) {
|
||||||
|
const body = await loginRes.text()
|
||||||
|
throw new Error(`Login failed (${loginRes.status}): ${body}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { token, expire } = await loginRes.json()
|
||||||
|
|
||||||
|
// Write a Playwright storage-state file containing the token in localStorage
|
||||||
|
const stateDir = path.join(__dirname, '.auth')
|
||||||
|
await mkdir(stateDir, { recursive: true })
|
||||||
|
await writeFile(
|
||||||
|
path.join(stateDir, 'state.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
cookies: [],
|
||||||
|
origins: [
|
||||||
|
{
|
||||||
|
origin: FRONTEND_URL,
|
||||||
|
localStorage: [
|
||||||
|
{ name: 'token', value: token },
|
||||||
|
{ name: 'token_expiry', value: expire },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('[global-setup] Test user ready, storage state saved.')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForServer(url, retries = 20, delayMs = 1000) {
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${url}/api/v1/auth/login`, { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
||||||
|
if (res.status < 500) return
|
||||||
|
} catch {
|
||||||
|
// server not up yet
|
||||||
|
}
|
||||||
|
console.log(`[global-setup] Waiting for server… (${i + 1}/${retries})`)
|
||||||
|
await new Promise(r => setTimeout(r, delayMs))
|
||||||
|
}
|
||||||
|
throw new Error(`Server at ${url} did not become ready in time`)
|
||||||
|
}
|
||||||
78
e2e/package-lock.json
generated
Normal file
78
e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"name": "donetick-e2e",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "donetick-e2e",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.44.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.59.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.59.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
e2e/package.json
Normal file
14
e2e/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "donetick-e2e",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "playwright test",
|
||||||
|
"test:headed": "playwright test --headed",
|
||||||
|
"test:report": "playwright show-report"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.44.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
e2e/playwright.config.js
Normal file
32
e2e/playwright.config.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
|
// When frontend and backend are the same origin (embedded binary) set E2E_BASE_URL.
|
||||||
|
// For dev, set E2E_FRONTEND_URL (default 5173) and E2E_API_URL (default 2021) separately.
|
||||||
|
const FRONTEND_URL =
|
||||||
|
process.env.E2E_FRONTEND_URL ||
|
||||||
|
process.env.E2E_BASE_URL ||
|
||||||
|
'http://localhost:5173'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
timeout: 30_000,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: 1, // sequential – single SQLite DB
|
||||||
|
reporter: process.env.CI ? 'github' : 'list',
|
||||||
|
|
||||||
|
use: {
|
||||||
|
baseURL: FRONTEND_URL,
|
||||||
|
headless: true,
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
},
|
||||||
|
|
||||||
|
globalSetup: './global-setup.js',
|
||||||
|
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
64
e2e/tests/auth.spec.js
Normal file
64
e2e/tests/auth.spec.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { signUpViaUI, loginViaUI } from '../fixtures/auth.js'
|
||||||
|
|
||||||
|
// Username must match /^[a-z.-]+$/ — no digits allowed.
|
||||||
|
// Generate a random lowercase-only suffix for uniqueness across runs.
|
||||||
|
function randomSuffix(len = 8) {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyz'
|
||||||
|
return Array.from({ length: len }, () =>
|
||||||
|
chars[Math.floor(Math.random() * 26)],
|
||||||
|
).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Auth – Sign Up', () => {
|
||||||
|
test('creates a new account and lands on /chores', async ({ page }) => {
|
||||||
|
const suffix = randomSuffix()
|
||||||
|
const user = {
|
||||||
|
username: `test.signup.${suffix}`,
|
||||||
|
email: `signup.${suffix}@donetick.test`,
|
||||||
|
password: 'TestPassword123!',
|
||||||
|
displayName: 'Test Signup User',
|
||||||
|
}
|
||||||
|
|
||||||
|
await signUpViaUI(page, user)
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/chores/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('shows an error when username is too short', async ({ page }) => {
|
||||||
|
await page.goto('/signup')
|
||||||
|
await page.locator('#username').fill('ab') // < 4 chars
|
||||||
|
await page.locator('#email').fill('short@donetick.test')
|
||||||
|
await page.locator('#password').fill('ValidPass123!')
|
||||||
|
await page.locator('#displayName').fill('Short User')
|
||||||
|
await page.getByRole('button', { name: 'Sign Up' }).click()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText('Username must be at least 4 characters'),
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Auth – Login', () => {
|
||||||
|
// Re-use the shared E2E user that global-setup already created
|
||||||
|
test('logs in with valid credentials and lands on /chores', async ({ page }) => {
|
||||||
|
await loginViaUI(page, {
|
||||||
|
username: 'e2e.user',
|
||||||
|
password: 'E2ePassword123!',
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/chores/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('shows an error for wrong password', async ({ page }) => {
|
||||||
|
await page.goto('/login')
|
||||||
|
await page.locator('#email').fill('e2e.user')
|
||||||
|
await page.locator('#password').fill('WrongPassword!')
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click()
|
||||||
|
|
||||||
|
// The notification snackbar / error message should appear
|
||||||
|
await expect(
|
||||||
|
page.getByText(/Login Failed|invalid|incorrect/i).first(),
|
||||||
|
).toBeVisible({ timeout: 5_000 })
|
||||||
|
})
|
||||||
|
})
|
||||||
53
e2e/tests/chores.spec.js
Normal file
53
e2e/tests/chores.spec.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { expect, test } from '../fixtures/auth.js'
|
||||||
|
import { API_URL } from '../global-setup.js'
|
||||||
|
|
||||||
|
test.describe('Chores – Create', () => {
|
||||||
|
// All tests in this suite run as the pre-authenticated E2E user
|
||||||
|
test.use({
|
||||||
|
storageState: '.auth/state.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
test('creates a daily recurring chore and confirms it appears in the list', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const choreName = `E2E Daily Chore ${Date.now()}`
|
||||||
|
|
||||||
|
// ── Navigate to the create chore page ────────────────────────────────
|
||||||
|
await page.goto('/chores/create')
|
||||||
|
|
||||||
|
// ── Fill in the chore name ────────────────────────────────────────────
|
||||||
|
// Name input is the first <input> on the page (no id/placeholder)
|
||||||
|
await page.locator('input').first().fill(choreName)
|
||||||
|
|
||||||
|
// ── Enable recurrence ─────────────────────────────────────────────────
|
||||||
|
// Check the "Repeat this task" checkbox to reveal frequency options
|
||||||
|
await page.getByLabel('Repeat this task').click()
|
||||||
|
|
||||||
|
// Select "Daily" from the frequency chips
|
||||||
|
await page.getByLabel('Daily').click()
|
||||||
|
|
||||||
|
// The due date is auto-populated to today once a repeating type is selected.
|
||||||
|
// No manual date entry required unless testing date-specific behaviour.
|
||||||
|
|
||||||
|
// ── Save ──────────────────────────────────────────────────────────────
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click()
|
||||||
|
|
||||||
|
// After save the app navigates back to the chore list
|
||||||
|
await page.waitForURL('**/chores', { timeout: 15_000 })
|
||||||
|
|
||||||
|
// ── Verify the chore appears in the UI ───────────────────────────────
|
||||||
|
await expect(page.getByText(choreName)).toBeVisible({ timeout: 10_000 })
|
||||||
|
|
||||||
|
// ── Verify the chore exists in the API ───────────────────────────────
|
||||||
|
const token = await page.evaluate(() => localStorage.getItem('token'))
|
||||||
|
const apiRes = await fetch(`${API_URL}/api/v1/chores/`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
expect(apiRes.ok).toBe(true)
|
||||||
|
|
||||||
|
const { res: chores } = await apiRes.json()
|
||||||
|
const created = chores.find(c => c.name === choreName)
|
||||||
|
expect(created).toBeDefined()
|
||||||
|
expect(created.frequencyType).toBe('daily')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user