Playwright UI test suite (25 tests) #1

Merged
m038 merged 7 commits from experimental-polar-steps into main 2026-06-18 22:41:35 +02:00
7 changed files with 139 additions and 1 deletions
Showing only changes of commit 545e3f5ba0 - Show all commits
+5
View File
@@ -11,5 +11,10 @@ user/plugins/
# Claude # Claude
.claude/ .claude/
# Tests
node_modules/
test-results/
playwright-report/
# OS # OS
.DS_Store .DS_Store
+4 -1
View File
@@ -14,7 +14,10 @@ test-config:
test-post: test-post:
@bash scripts/test-post.sh @bash scripts/test-post.sh
test: test-config test-post test-ui:
@npx playwright test
test: test-config test-post test-ui
# ── Local dev ────────────────────────────────────────────────────────────────── # ── Local dev ──────────────────────────────────────────────────────────────────
+10
View File
@@ -0,0 +1,10 @@
{
"name": "intotheeast-tests",
"private": true,
"scripts": {
"test:ui": "playwright test"
},
"devDependencies": {
"@playwright/test": "^1.48.0"
}
}
+16
View File
@@ -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']],
});
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

+14
View File
@@ -0,0 +1,14 @@
const fs = require('fs');
const path = require('path');
module.exports = async function globalSetup() {
const envFile = path.join(__dirname, '../.env');
if (fs.existsSync(envFile)) {
fs.readFileSync(envFile, 'utf-8').split(/\r?\n/).forEach(line => {
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');
}
});
}
};
+90
View File
@@ -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');
});