From 545e3f5ba04fd296a96a5bf9003d592d8695d180 Mon Sep 17 00:00:00 2001 From: Mischa Date: Thu, 18 Jun 2026 20:35:17 +0200 Subject: [PATCH] feat: add Playwright UI test for post-with-photo flow --- .gitignore | 5 ++ Makefile | 5 +- package.json | 10 ++++ playwright.config.js | 16 ++++++ tests/fixtures/test-photo.jpg | Bin 0 -> 118 bytes tests/global-setup.js | 14 +++++ tests/ui/post-with-photo.spec.js | 90 +++++++++++++++++++++++++++++++ 7 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 package.json create mode 100644 playwright.config.js create mode 100644 tests/fixtures/test-photo.jpg create mode 100644 tests/global-setup.js create mode 100644 tests/ui/post-with-photo.spec.js diff --git a/.gitignore b/.gitignore index 2448cf0..a8c0530 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,10 @@ user/plugins/ # Claude .claude/ +# Tests +node_modules/ +test-results/ +playwright-report/ + # OS .DS_Store diff --git a/Makefile b/Makefile index 0b66d8e..6954c24 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,10 @@ test-config: test-post: @bash scripts/test-post.sh -test: test-config test-post +test-ui: + @npx playwright test + +test: test-config test-post test-ui # ── Local dev ────────────────────────────────────────────────────────────────── diff --git a/package.json b/package.json new file mode 100644 index 0000000..1a9a6be --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "intotheeast-tests", + "private": true, + "scripts": { + "test:ui": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.48.0" + } +} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..befa732 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,16 @@ +// @ts-check +const { defineConfig } = require('@playwright/test'); + +module.exports = defineConfig({ + testDir: './tests/ui', + globalSetup: './tests/global-setup.js', + timeout: 30_000, + retries: 0, + use: { + baseURL: process.env.GRAV_BASE_URL || 'http://localhost:8081', + headless: true, + screenshot: 'only-on-failure', + video: 'off', + }, + reporter: [['line']], +}); diff --git a/tests/fixtures/test-photo.jpg b/tests/fixtures/test-photo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ff333a8e2180f586d8311da5845952c083ce6a72 GIT binary patch literal 118 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R< { + const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/); + if (m && !process.env[m[1]]) { + process.env[m[1]] = m[2].trim().replace(/^(['"])(.*)\1$/, '$2'); + } + }); + } +}; diff --git a/tests/ui/post-with-photo.spec.js b/tests/ui/post-with-photo.spec.js new file mode 100644 index 0000000..529de5c --- /dev/null +++ b/tests/ui/post-with-photo.spec.js @@ -0,0 +1,90 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); +const path = require('path'); +const fs = require('fs'); + + +const USER = process.env.GRAV_TEST_USER; +const PASS = process.env.GRAV_TEST_PASS; +const TRACKER_DIR = path.join(__dirname, '../../user/pages/01.tracker'); +const TEST_PHOTO = path.join(__dirname, '../fixtures/test-photo.jpg'); + +let createdSlug = null; + +test.beforeAll(() => { + if (!USER || !PASS) throw new Error('GRAV_TEST_USER and GRAV_TEST_PASS must be set in .env'); +}); + +test.afterAll(() => { + if (createdSlug) { + const dir = path.join(TRACKER_DIR, createdSlug); + if (fs.existsSync(dir)) { + fs.rmSync(dir, { recursive: true }); + console.log(` [cleanup] removed ${createdSlug}`); + } + } +}); + +test('login, post entry with photo, verify in tracker', async ({ page }) => { + // ── 1. Login ───────────────────────────────────────────────────────────── + await page.goto('/login'); + await page.fill('input[name="username"]', USER); + await page.fill('input[name="password"]', PASS); + await page.click('input[type="submit"], button[type="submit"]'); + // Wait for login to complete — page leaves /login regardless of redirect target + await page.waitForFunction(() => !window.location.pathname.includes('/login'), { timeout: 10_000 }); + await expect(page.locator('.site-header')).toBeVisible(); + + // ── 2. Navigate to post form ───────────────────────────────────────────── + await page.goto('/post'); + await expect(page.locator('.post-form-wrap')).toBeVisible(); + + // ── 3. Fill in required fields ──────────────────────────────────────────── + const title = `UI Test ${Date.now()}`; + await page.fill('input[name="data[title]"]', title); + await page.fill('textarea[name="data[content]"]', 'Automated UI test entry. Safe to delete.'); + await page.fill('input[name="data[location_city]"]', 'Testville'); + await page.fill('input[name="data[location_country]"]', 'Testland'); + + // ── 4. Upload photo via filepond ───────────────────────────────────────── + const fileInput = page.locator('input[type="file"]').first(); + await fileInput.setInputFiles(TEST_PHOTO); + + // Wait for filepond to finish the XHR upload (status indicator disappears) + await page.waitForFunction(() => { + const items = document.querySelectorAll('.filepond--item[data-filepond-item-state]'); + return [...items].every(el => el.getAttribute('data-filepond-item-state') === 'idle'); + }, { timeout: 15_000 }); + + // ── 5. Submit ──────────────────────────────────────────────────────────── + await page.click('.btn-post'); + + // Wait for success message or redirect back to /post + await page.waitForSelector('.form-messages, .notices', { timeout: 15_000 }); + + // ── 6. Verify entry created on disk ────────────────────────────────────── + const todayPrefix = new Date().toISOString().slice(0, 10); // YYYY-MM-DD + const entries = fs.readdirSync(TRACKER_DIR).filter(d => d.startsWith(todayPrefix)); + + // Find the one matching our title slug + const titleSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); + const match = entries.find(e => e.includes('ui-test')); + expect(match, 'Entry folder should exist on disk').toBeTruthy(); + createdSlug = match; + + // ── 7. Verify entry.md (or entry.en.md) contains the title ─────────────── + const entryDir = path.join(TRACKER_DIR, match); + const mdFile = ['entry.md', 'entry.en.md'].find(f => fs.existsSync(path.join(entryDir, f))); + expect(mdFile, 'entry markdown file should exist').toBeTruthy(); + const mdContent = fs.readFileSync(path.join(entryDir, mdFile), 'utf-8'); + expect(mdContent).toContain('UI Test'); + + // ── 8. Verify photo was saved ───────────────────────────────────────────── + const files = fs.readdirSync(entryDir); + const photos = files.filter(f => /\.(jpg|jpeg|png|webp|heic)$/i.test(f)); + expect(photos.length, 'At least one photo should be saved in the entry folder').toBeGreaterThan(0); + + // ── 9. Verify entry appears on /tracker ─────────────────────────────────── + await page.goto('/tracker'); + await expect(page.locator('body')).toContainText('UI Test'); +});