refactor: migrate to monorepo structure with Docker, PocketBase, and e2e tests

- Reorganize into apps/, packages/, docs/, e2e/, pocketbase/ directories
- Add Dockerfiles for web, worker, and PocketBase services
- Add docker-compose.yml for local orchestration
- Add turbo.json for monorepo task management
- Add Playwright e2e test infrastructure
- Add PocketBase backend with migrations
- Remove Vite/Next.js/ESLint/PostCSS config files
- Update package.json with workspace dependencies
- Add .env.example and .dockerignore
This commit is contained in:
2026-07-16 06:19:58 -04:00
parent ec14645a4b
commit 8f55626e03
286 changed files with 31992 additions and 9245 deletions
+152
View File
@@ -0,0 +1,152 @@
import { test, expect } from '@playwright/test';
import { login } from './helpers/auth';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
test.describe('Import/Export', () => {
test.beforeEach(async ({ page }) => {
await login(page);
await page.goto('/settings');
// Wait for the settings page to load
await expect(page.getByRole('heading', { name: /settings/i })).toBeVisible();
// Switch to Import & Export tab
await page.getByRole('tab', { name: /import.*export/i }).click();
});
test.describe('Export', () => {
test('should show export section with collection selection', async ({ page }) => {
await expect(page.getByText(/export data/i)).toBeVisible();
await expect(page.getByLabel(/select all/i)).toBeVisible();
await expect(page.getByRole('button', { name: /export to json/i })).toBeVisible();
});
test('should have all collection checkboxes', async ({ page }) => {
const collections = [
'tasks', 'habits', 'projects', 'notes', 'reports',
'milestones', 'domains', 'tags', 'agents', 'webhooks',
];
for (const collection of collections) {
await expect(page.getByLabel(new RegExp(`export ${collection}`, 'i'))).toBeVisible();
}
});
test('should toggle select all checkbox', async ({ page }) => {
const selectAll = page.getByLabel(/select all/i);
await expect(selectAll).toBeChecked();
// Uncheck select all
await selectAll.click();
await page.waitForTimeout(300);
// Individual checkboxes should be unchecked
const tasksCheckbox = page.getByLabel(/export tasks/i);
await expect(tasksCheckbox).not.toBeChecked();
// Check select all again
await selectAll.click();
await page.waitForTimeout(300);
await expect(tasksCheckbox).toBeChecked();
});
test('should export data as JSON', async ({ page }) => {
// Set up download handler
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: /export to json/i }).click();
try {
const download = await downloadPromise;
// Verify download started
const filename = download.suggestedFilename();
expect(filename).toMatch(/project-e-export.*\.json/);
} catch {
// Export might fail if PocketBase isn't running that's OK for structure test
}
});
});
test.describe('Import', () => {
test('should show import section', async ({ page }) => {
await expect(page.getByText(/import data/i)).toBeVisible();
await expect(page.getByText(/restore from a previously exported/i)).toBeVisible();
await expect(page.getByRole('button', { name: /import from json/i })).toBeVisible();
});
test('should have file upload input', async ({ page }) => {
// The file input is hidden, triggered by the button
const fileInput = page.locator('input[type="file"][accept=".json"]');
await expect(fileInput).toBeAttached();
});
test('should import data from JSON file', async ({ page }) => {
// Create a minimal valid import file
const importData = {
version: 1,
exported_at: new Date().toISOString(),
collections: {
tasks: [],
habits: [],
projects: [],
notes: [],
reports: [],
},
};
const tempDir = os.tmpdir();
const importFile = path.join(tempDir, `e2e-import-${Date.now()}.json`);
fs.writeFileSync(importFile, JSON.stringify(importData, null, 2));
try {
// Trigger file upload
const fileInput = page.locator('input[type="file"][accept=".json"]');
await fileInput.setInputFiles(importFile);
// Confirmation dialog should appear
await expect(page.getByRole('dialog', { name: /confirm import/i })).toBeVisible({ timeout: 5_000 });
// Click Import button in dialog
await page.getByRole('button', { name: /import$/i }).click();
// Wait for import to complete
await page.waitForTimeout(3_000);
} finally {
// Cleanup temp file
try { fs.unlinkSync(importFile); } catch { /* ignore */ }
}
});
test('should show import confirmation dialog', async ({ page }) => {
const importData = {
version: 1,
exported_at: new Date().toISOString(),
collections: { tasks: [], habits: [], projects: [], notes: [], reports: [] },
};
const tempDir = os.tmpdir();
const importFile = path.join(tempDir, `e2e-import-dialog-${Date.now()}.json`);
fs.writeFileSync(importFile, JSON.stringify(importData, null, 2));
try {
const fileInput = page.locator('input[type="file"][accept=".json"]');
await fileInput.setInputFiles(importFile);
// Confirmation dialog
const dialog = page.getByRole('dialog', { name: /confirm import/i });
await expect(dialog).toBeVisible({ timeout: 5_000 });
// Should have Cancel and Import buttons
await expect(dialog.getByRole('button', { name: /cancel/i })).toBeVisible();
await expect(dialog.getByRole('button', { name: /import/i })).toBeVisible();
// Cancel the import
await dialog.getByRole('button', { name: /cancel/i }).click();
await expect(dialog).not.toBeVisible({ timeout: 3_000 });
} finally {
try { fs.unlinkSync(importFile); } catch { /* ignore */ }
}
});
});
});