Initial commit: Brachnha Insight project setup

- Next.js 14+ with App Router and TypeScript
- Tailwind CSS and ShadCN UI styling
- Zustand state management
- Dexie.js for IndexedDB (local-first data)
- Auth.js v5 for authentication
- BMAD framework integration

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Max
2026-01-26 12:28:43 +07:00
commit 3fbbb1a93b
812 changed files with 150531 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
import { test, expect } from '@playwright/test';
test.describe('Deletion Persistence', () => {
test('should permanently remove item from Dexie DB', async () => {
// GIVEN: Item exists in DB
// WHEN: Delete action (via Service or UI)
// THEN: Item is gone from DB
expect(true).toBe(false);
});
});

View File

@@ -0,0 +1,27 @@
import { test, expect } from '@playwright/test';
test.describe('Ghostwriter Persistence', () => {
test('Scenario 2.4.2 (P0): Session Saved as Completed', async ({ request }) => {
// GIVEN: A chat session flow is completed
// This is an API/Integration test, so we hit the endpoint directly if possible,
// OR we use the service capability if we were in a unit test.
// For Playwright Integration (API), we assume an endpoint exists to finalize.
// Setup: Create a session
const createRes = await request.post('/api/sessions', { data: { title: 'Test Session' } });
const { sessionId } = await createRes.json();
// WHEN: User saves the final draft
const saveRes = await request.post(`/api/sessions/${sessionId}/finalize`, {
data: { finalContent: 'My Lesson' }
});
expect(saveRes.status()).toBe(200);
// THEN: Session status is COMPLETED
const getRes = await request.get(`/api/sessions/${sessionId}`);
const session = await getRes.json();
expect(session.status).toBe('COMPLETED');
expect(session.finalArtifact).toBe('My Lesson');
});
});

View File

@@ -0,0 +1,79 @@
import { expect } from '@playwright/test';
import { test } from '../support/fixtures/offline.fixture';
test.describe('Offline Action Queueing', () => {
test('should enqueue SAVE_DRAFT action when offline', async ({ offlineControl, page }) => {
// GIVEN: App is loaded
await page.goto('/');
await page.waitForFunction(() => (window as any).db !== undefined && (window as any).DraftService !== undefined, { timeout: 10000 });
// GIVEN: App goes offline
await offlineControl.goOffline(page.context());
// WHEN: DraftService.saveDraft is called (Service Integration)
await page.evaluate(async () => {
// @ts-ignore
await window.DraftService.saveDraft({
title: 'Offline Draft',
content: 'Content written offline',
tags: [],
createdAt: Date.now(),
status: 'draft',
sessionId: 'test-session'
});
});
// THEN: 'syncQueue' table has 1 item
const queueCount = await page.evaluate(async () => {
// @ts-ignore
return await window.db.syncQueue.count();
});
expect(queueCount).toBe(1);
const firstItem = await page.evaluate(async () => {
// @ts-ignore
return await window.db.syncQueue.orderBy('createdAt').first();
});
expect(firstItem.action).toBe('saveDraft');
expect(firstItem.payload.draftData.title).toBe('Offline Draft');
});
test('should enqueue DELETE_ENTRY action when offline', async ({ offlineControl, page }) => {
// GIVEN: App is loaded and has an entry
await page.goto('/');
await page.waitForFunction(() => (window as any).db !== undefined && (window as any).DraftService !== undefined, { timeout: 10000 });
// Setup entry
const draftId = await page.evaluate(async () => {
// @ts-ignore
const id = await window.db.drafts.add({
title: 'To Delete',
content: 'Delete me',
tags: [],
createdAt: Date.now(),
status: 'draft',
sessionId: 'test-session'
});
return id;
});
await offlineControl.goOffline(page.context());
// WHEN: DraftService.deleteDraft is called
await page.evaluate(async (id) => {
// @ts-ignore
await window.DraftService.deleteDraft(id);
}, draftId);
// THEN: SyncQueue has delete action
const lastItem = await page.evaluate(async () => {
// @ts-ignore
return await window.db.syncQueue.orderBy('createdAt').last();
});
expect(lastItem.action).toBe('deleteDraft');
expect(lastItem.payload.draftId).toBe(draftId);
});
});

View File

@@ -0,0 +1,45 @@
import { test, expect } from '@playwright/test';
test.describe('PWA Manifest', () => {
test('should serve a valid manifest.webmanifest', async ({ request }) => {
const response = await request.get('/manifest.webmanifest');
expect(response.status()).toBe(200);
expect(response.headers()['content-type']).toContain('application/manifest+json');
const manifest = await response.json();
// Core properties matching manifest.ts
expect(manifest.name).toBe('Test01');
expect(manifest.short_name).toBe('Test01');
expect(manifest.display).toBe('standalone');
expect(manifest.start_url).toBe('/');
expect(manifest.background_color).toBe('#F8FAFC');
expect(manifest.theme_color).toBe('#64748B');
// Icons
expect(manifest.icons.length).toBeGreaterThanOrEqual(2);
expect(manifest.icons).toEqual(
expect.arrayContaining([
expect.objectContaining({
src: '/icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png'
}),
expect.objectContaining({
src: '/icons/icon-512x512.png',
sizes: '512x512',
type: 'image/png'
})
])
);
});
test('should have accessible icon files', async ({ request }) => {
const icon192 = await request.get('/icons/icon-192x192.png');
expect(icon192.status()).toBe(200);
const icon512 = await request.get('/icons/icon-512x512.png');
expect(icon512.status()).toBe(200);
});
});

View File

@@ -0,0 +1,40 @@
import { expect } from '@playwright/test';
import { test } from '../support/fixtures/offline.fixture';
test.describe('Sync Action Replay', () => {
test('should process pending actions when network restores', async ({ offlineControl, page }) => {
// GIVEN: Offline queue has actions
await page.goto('/'); // Ensure page is loaded to initialize DB
await page.waitForFunction(() => (window as any).db !== undefined, { timeout: 10000 });
await offlineControl.goOffline(page.context());
// Seed queue via evaluate
await page.evaluate(async () => {
// @ts-ignore
await window.db.syncQueue.add({
action: 'saveDraft',
payload: { draftData: { title: 'Pending Sync', content: '...' } },
status: 'pending',
createdAt: Date.now(),
retries: 0
});
});
// WHEN: Network restores
await offlineControl.goOnline(page.context());
// Trigger sync - The SyncManager listens to 'online' event.
// Note: The 'online' event listeners might execute async.
// Wait for processing
// We can poll the DB
await expect.poll(async () => {
return await page.evaluate(async () => {
// @ts-ignore
return await window.db.syncQueue.where('status').equals('pending').count();
});
}, { timeout: 5000 }).toBe(0);
});
});