test: reorganise tests/ui/ into feature subdirectories
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
// @ts-check
|
||||
// Tests: P1–P5 — form submission happy paths
|
||||
// Replaces post-with-photo.spec.js
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { waitForFilePondUpload, cleanupEntry, findEntry, readEntryMd, TRACKER_DIR } = require('../helpers');
|
||||
|
||||
const TEST_PHOTO = path.join(__dirname, '../fixtures/test-photo.jpg');
|
||||
|
||||
// Track slugs created per test for cleanup
|
||||
const created = [];
|
||||
|
||||
test.afterAll(() => {
|
||||
created.forEach(cleanupEntry);
|
||||
});
|
||||
|
||||
// ── P1: Post without photo ─────────────────────────────────────────────────────
|
||||
test('P1: post text-only entry → created on disk and visible on /dailies', async ({ page }) => {
|
||||
const tag = `p1-${Date.now()}`;
|
||||
const title = `UI Test ${tag}`;
|
||||
|
||||
await page.goto('/post');
|
||||
await page.fill('input[name="data[title]"]', title);
|
||||
await page.fill('textarea[name="data[content]"]', 'Text-only test entry. Safe to delete.');
|
||||
await page.fill('input[name="data[location_city]"]', 'Testville');
|
||||
await page.fill('input[name="data[location_country]"]', 'Testland');
|
||||
await page.locator('.btn-post').evaluate(el => el.click());
|
||||
await page.waitForSelector('.form-messages, .notices', { timeout: 15_000 });
|
||||
|
||||
const entryDir = findEntry(tag);
|
||||
expect(entryDir, 'Entry folder should exist on disk').toBeTruthy();
|
||||
created.push(tag);
|
||||
|
||||
const md = readEntryMd(entryDir);
|
||||
expect(md).toContain(tag);
|
||||
|
||||
// No photo expected
|
||||
const photos = fs.readdirSync(entryDir).filter(f => /\.(jpg|jpeg|png|webp|heic)$/i.test(f));
|
||||
expect(photos.length, 'Text-only entry should have no photos').toBe(0);
|
||||
|
||||
await page.goto('/trips/japan-korea-2026/dailies');
|
||||
await expect(page.locator('body')).toContainText(tag);
|
||||
});
|
||||
|
||||
// ── P2: Post with photo ────────────────────────────────────────────────────────
|
||||
test('P2: post entry with photo → photo saved in entry folder and visible on /dailies', async ({ page }) => {
|
||||
const tag = `p2-${Date.now()}`;
|
||||
const title = `UI Test ${tag}`;
|
||||
|
||||
await page.goto('/post');
|
||||
await page.fill('input[name="data[title]"]', title);
|
||||
await page.fill('textarea[name="data[content]"]', 'Photo test entry. Safe to delete.');
|
||||
await page.fill('input[name="data[location_city]"]', 'Testville');
|
||||
await page.fill('input[name="data[location_country]"]', 'Testland');
|
||||
|
||||
await page.locator('input.filepond--browser').setInputFiles(TEST_PHOTO);
|
||||
await waitForFilePondUpload(page);
|
||||
|
||||
await page.locator('.btn-post').evaluate(el => el.click());
|
||||
await page.waitForSelector('.form-messages, .notices', { timeout: 15_000 });
|
||||
|
||||
const entryDir = findEntry(tag);
|
||||
expect(entryDir, 'Entry folder should exist on disk').toBeTruthy();
|
||||
created.push(tag);
|
||||
|
||||
const md = readEntryMd(entryDir);
|
||||
expect(md).toContain(tag);
|
||||
|
||||
const photos = fs.readdirSync(entryDir).filter(f => /\.(jpg|jpeg|png|webp|heic)$/i.test(f));
|
||||
expect(photos.length, 'At least one photo should be saved').toBeGreaterThan(0);
|
||||
|
||||
await page.goto('/trips/japan-korea-2026/dailies');
|
||||
await expect(page.locator('body')).toContainText(tag);
|
||||
});
|
||||
|
||||
// ── P3: Post with city + country ──────────────────────────────────────────────
|
||||
test('P3: post entry with city/country → frontmatter contains location', async ({ page }) => {
|
||||
const tag = `p3-${Date.now()}`;
|
||||
const title = `UI Test ${tag}`;
|
||||
|
||||
await page.goto('/post');
|
||||
await page.fill('input[name="data[title]"]', title);
|
||||
await page.fill('textarea[name="data[content]"]', 'Location test. Safe to delete.');
|
||||
await page.fill('input[name="data[location_city]"]', 'Kyoto');
|
||||
await page.fill('input[name="data[location_country]"]', 'Japan');
|
||||
await page.locator('.btn-post').evaluate(el => el.click());
|
||||
await page.waitForSelector('.form-messages, .notices', { timeout: 15_000 });
|
||||
|
||||
const entryDir = findEntry(tag);
|
||||
expect(entryDir, 'Entry folder should exist on disk').toBeTruthy();
|
||||
created.push(tag);
|
||||
|
||||
const md = readEntryMd(entryDir);
|
||||
expect(md).toContain('Kyoto');
|
||||
expect(md).toContain('Japan');
|
||||
});
|
||||
|
||||
// ── P4: Post with lat/lng ─────────────────────────────────────────────────────
|
||||
test('P4: post entry with lat/lng → coordinates saved in frontmatter', async ({ page }) => {
|
||||
const tag = `p4-${Date.now()}`;
|
||||
const title = `UI Test ${tag}`;
|
||||
|
||||
await page.goto('/post');
|
||||
await page.fill('input[name="data[title]"]', title);
|
||||
await page.fill('textarea[name="data[content]"]', 'GPS test. Safe to delete.');
|
||||
// lat/lng fields are CSS-hidden (designed to be filled by the Get Location button);
|
||||
// set values directly via JS to simulate what the button would do.
|
||||
await page.evaluate(() => {
|
||||
document.querySelector('input[name="data[lat]"]').value = '35.6762';
|
||||
document.querySelector('input[name="data[lng]"]').value = '139.6503';
|
||||
});
|
||||
await page.locator('.btn-post').evaluate(el => el.click());
|
||||
await page.waitForSelector('.form-messages, .notices', { timeout: 15_000 });
|
||||
|
||||
const entryDir = findEntry(tag);
|
||||
expect(entryDir, 'Entry folder should exist on disk').toBeTruthy();
|
||||
created.push(tag);
|
||||
|
||||
const md = readEntryMd(entryDir);
|
||||
expect(md).toContain('35.6762');
|
||||
expect(md).toContain('139.6503');
|
||||
});
|
||||
|
||||
// ── P5: "Get Location" button fills lat/lng (TDD — fails until feature built) ──
|
||||
test('P5: Get Location button fills lat/lng from browser geolocation', async ({ page, context }) => {
|
||||
await context.grantPermissions(['geolocation']);
|
||||
await context.setGeolocation({ latitude: 35.6762, longitude: 139.6503 });
|
||||
|
||||
await page.goto('/post');
|
||||
await page.click('#get-location');
|
||||
|
||||
// Allow a moment for the geolocation callback to fire
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(page.locator('input[name="data[lat]"]')).toHaveValue(/35\.67/);
|
||||
await expect(page.locator('input[name="data[lng]"]')).toHaveValue(/139\.65/);
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
// @ts-check
|
||||
// Tests: V1–V4 — form validation and input constraints
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const path = require('path');
|
||||
|
||||
const TEST_PHOTO = path.join(__dirname, '../fixtures/test-photo.jpg');
|
||||
const TEST_NONIMAGE = path.join(__dirname, '../fixtures/test-nonimage.txt');
|
||||
|
||||
// ── V1: Missing title ─────────────────────────────────────────────────────────
|
||||
test('V1: submit without title shows a validation error or stays on /post', async ({ page }) => {
|
||||
await page.goto('/post');
|
||||
// Leave title empty, fill only content
|
||||
await page.fill('textarea[name="data[content]"]', 'Content without a title.');
|
||||
await page.locator('.btn-post').evaluate(el => el.click());
|
||||
|
||||
// Grav either shows an error message OR re-renders the form (stays on /post).
|
||||
// Either outcome is acceptable — what should NOT happen is a success message.
|
||||
await page.waitForTimeout(2_000);
|
||||
const bodyText = await page.locator('body').textContent();
|
||||
expect(bodyText).not.toContain('Entry posted successfully');
|
||||
});
|
||||
|
||||
// ── V2: Missing content ───────────────────────────────────────────────────────
|
||||
test('V2: submit without content shows a validation error or stays on /post', async ({ page }) => {
|
||||
await page.goto('/post');
|
||||
await page.fill('input[name="data[title]"]', 'V2 title no content');
|
||||
// Leave content (textarea) empty
|
||||
await page.locator('.btn-post').evaluate(el => el.click());
|
||||
|
||||
await page.waitForTimeout(2_000);
|
||||
const bodyText = await page.locator('body').textContent();
|
||||
expect(bodyText).not.toContain('Entry posted successfully');
|
||||
});
|
||||
|
||||
// ── V3: Photo limit (max 4) ───────────────────────────────────────────────────
|
||||
test('V3: filepond rejects a 5th photo when limit is 4', async ({ page }) => {
|
||||
await page.goto('/post');
|
||||
|
||||
// Upload 4 photos (all the same fixture — we just need 4 items)
|
||||
const fourPhotos = [TEST_PHOTO, TEST_PHOTO, TEST_PHOTO, TEST_PHOTO];
|
||||
await page.locator('input.filepond--browser').setInputFiles(fourPhotos);
|
||||
|
||||
// Wait for all 4 items to appear
|
||||
await page.waitForFunction(() =>
|
||||
document.querySelectorAll('.filepond--item').length === 4,
|
||||
{ timeout: 10_000 }
|
||||
);
|
||||
|
||||
// Attempt a 5th — filepond should ignore it once the limit is reached
|
||||
await page.locator('input.filepond--browser').setInputFiles([TEST_PHOTO]);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const itemCount = await page.locator('.filepond--item').count();
|
||||
expect(itemCount).toBe(4);
|
||||
});
|
||||
|
||||
// ── V4: Non-image file rejected ───────────────────────────────────────────────
|
||||
test('V4: filepond rejects non-image files', async ({ page }) => {
|
||||
await page.goto('/post');
|
||||
|
||||
await page.locator('input.filepond--browser').setInputFiles(TEST_NONIMAGE);
|
||||
await page.waitForTimeout(1_000);
|
||||
|
||||
const items = page.locator('.filepond--item');
|
||||
const count = await items.count();
|
||||
|
||||
if (count > 0) {
|
||||
// If filepond added it, it must show an error state — not processing-complete
|
||||
const state = await items.first().getAttribute('data-filepond-item-state');
|
||||
expect(state).not.toBe('processing-complete');
|
||||
} else {
|
||||
// Silently rejected before adding — also a pass
|
||||
expect(count).toBe(0);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user