- Rename TASKPLANER_DATA_DIR to DATA_DIR (avoid adapter-node envPrefix conflict) - Add TASKPLANER_ORIGIN for CSRF protection in docker-compose.yml - Add automatic database schema initialization on startup - Add Playwright E2E tests for Docker deployment verification - Update .env.example with correct variable names Fixes container restart loop and 403 errors on form submission. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
110 lines
3.5 KiB
TypeScript
110 lines
3.5 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { exec } from 'child_process';
|
|
import { promisify } from 'util';
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
/**
|
|
* Docker deployment tests
|
|
*
|
|
* These tests verify the application works correctly when deployed in Docker.
|
|
* They specifically test CSRF protection which requires TASKPLANER_ORIGIN to be set.
|
|
*
|
|
* Run against Docker: BASE_URL=http://localhost:3000 npx playwright test
|
|
*/
|
|
|
|
test.describe('Docker Deployment', () => {
|
|
test('application loads', async ({ page }) => {
|
|
await page.goto('/');
|
|
await expect(page).toHaveTitle(/TaskPlaner/i);
|
|
});
|
|
|
|
test('health endpoint returns ok', async ({ request }) => {
|
|
const response = await request.get('/health');
|
|
expect(response.status()).toBe(200);
|
|
expect(await response.text()).toBe('ok');
|
|
});
|
|
|
|
test('can create entry via form submission', async ({ page }) => {
|
|
// This test verifies CSRF protection is properly configured
|
|
// If TASKPLANER_ORIGIN is not set, form submissions return 403
|
|
await page.goto('/');
|
|
|
|
// Fill in quick capture form (textarea for content)
|
|
const contentInput = page.locator('textarea[name="content"]');
|
|
await contentInput.fill('Test entry from Playwright');
|
|
|
|
// Submit the form - button text is "Add"
|
|
const addButton = page.locator('button[type="submit"]:has-text("Add")');
|
|
await addButton.click();
|
|
|
|
// Wait for the entry to appear in the list
|
|
await expect(page.locator('text=Test entry from Playwright')).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('can toggle entry completion', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Create a task entry first
|
|
const contentInput = page.locator('textarea[name="content"]');
|
|
await contentInput.fill('Test task for completion');
|
|
|
|
// Select task type
|
|
const typeSelect = page.locator('select[name="type"]');
|
|
await typeSelect.selectOption('task');
|
|
|
|
const addButton = page.locator('button[type="submit"]:has-text("Add")');
|
|
await addButton.click();
|
|
|
|
// Wait for entry to appear
|
|
await expect(page.locator('text=Test task for completion')).toBeVisible({ timeout: 5000 });
|
|
|
|
// Find and click the checkbox to mark complete
|
|
const checkbox = page.locator('input[type="checkbox"]').first();
|
|
await checkbox.click();
|
|
|
|
// Verify the action completed (no 403 error)
|
|
// The checkbox should now be checked
|
|
await expect(checkbox).toBeChecked({ timeout: 5000 });
|
|
});
|
|
|
|
test('data persists across container restart', async ({ page }) => {
|
|
// This test verifies Docker volume persistence
|
|
const uniqueContent = `Persistence test ${Date.now()}`;
|
|
|
|
// Create an entry
|
|
await page.goto('/');
|
|
const contentInput = page.locator('textarea[name="content"]');
|
|
await contentInput.fill(uniqueContent);
|
|
|
|
const addButton = page.locator('button[type="submit"]:has-text("Add")');
|
|
await addButton.click();
|
|
|
|
// Wait for entry to appear
|
|
await expect(page.locator(`text=${uniqueContent}`)).toBeVisible({ timeout: 5000 });
|
|
|
|
// Restart the container
|
|
await execAsync('docker compose restart');
|
|
|
|
// Wait for container to be healthy again
|
|
let healthy = false;
|
|
for (let i = 0; i < 30; i++) {
|
|
try {
|
|
const response = await fetch('http://localhost:3000/health');
|
|
if (response.ok) {
|
|
healthy = true;
|
|
break;
|
|
}
|
|
} catch {
|
|
// Container not ready yet
|
|
}
|
|
await new Promise((r) => setTimeout(r, 1000));
|
|
}
|
|
expect(healthy).toBe(true);
|
|
|
|
// Verify entry still exists after restart
|
|
await page.goto('/');
|
|
await expect(page.locator(`text=${uniqueContent}`)).toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|