feat: add Playwright UI test for post-with-photo flow
This commit is contained in:
@@ -11,5 +11,10 @@ user/plugins/
|
||||
# Claude
|
||||
.claude/
|
||||
|
||||
# Tests
|
||||
node_modules/
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "intotheeast-tests",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test:ui": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.48.0"
|
||||
}
|
||||
}
|
||||
@@ -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']],
|
||||
});
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 118 B |
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
Reference in New Issue
Block a user