chore(web): add Dockerfile, service worker, and e2e test scaffolds

This commit is contained in:
saravanakumardb1 2026-03-27 11:28:33 -07:00
parent d179c4c624
commit 5453cb131a
7 changed files with 214 additions and 1 deletions

View File

@ -21,6 +21,10 @@ COPY web/next-env.d.ts ./next-env.d.ts
COPY web/src/ ./src/ COPY web/src/ ./src/
COPY shared/ ../shared/ COPY shared/ ../shared/
ARG NEXT_PUBLIC_BACKEND_URL
ARG NEXT_PUBLIC_PLATFORM_SERVICE_URL
ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL
ENV NEXT_PUBLIC_PLATFORM_SERVICE_URL=$NEXT_PUBLIC_PLATFORM_SERVICE_URL
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm run build RUN pnpm run build

43
web/e2e/auth.spec.ts Normal file
View File

@ -0,0 +1,43 @@
import { test, expect } from '@playwright/test';
test.describe('Auth — Settings Page', () => {
test('settings page shows auth form when not logged in', async ({ page }) => {
await page.goto('/settings');
await page.waitForLoadState('networkidle');
await expect(page.getByText('Account & Sync')).toBeVisible({ timeout: 30_000 });
await expect(page.getByText(/sign in to sync/i)).toBeVisible();
});
test('shows Sign In and Create Account toggle buttons', async ({ page }) => {
await page.goto('/settings');
await page.waitForLoadState('networkidle');
await expect(page.getByText('Account & Sync')).toBeVisible({ timeout: 30_000 });
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Create Account' })).toBeVisible();
});
test('shows email and password fields in login mode', async ({ page }) => {
await page.goto('/settings');
await page.waitForLoadState('networkidle');
await expect(page.getByText('Account & Sync')).toBeVisible({ timeout: 30_000 });
await expect(page.getByPlaceholder('Email')).toBeVisible();
await expect(page.getByPlaceholder(/password/i).first()).toBeVisible();
});
test('switching to register mode shows name field', async ({ page }) => {
await page.goto('/settings');
await page.waitForLoadState('networkidle');
await expect(page.getByText('Account & Sync')).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: 'Create Account' }).click();
await expect(page.getByPlaceholder('Display name')).toBeVisible();
});
test('forgot password link switches to reset mode', async ({ page }) => {
await page.goto('/settings');
await page.waitForLoadState('networkidle');
await expect(page.getByText('Account & Sync')).toBeVisible({ timeout: 30_000 });
await page.getByText('Forgot password?').click();
await expect(page.getByRole('button', { name: 'Send Reset Link' })).toBeVisible();
await expect(page.getByText('Back to sign in')).toBeVisible();
});
});

41
web/e2e/landing.spec.ts Normal file
View File

@ -0,0 +1,41 @@
import { test, expect } from '@playwright/test';
test.describe('Landing Page', () => {
test('renders hero section with branding', async ({ page }) => {
await page.goto('/landing');
await expect(page.getByText('ChronoMind')).toBeVisible();
await expect(page.getByText(/never be caught/i)).toBeVisible();
});
test('shows feature grid with 6 features', async ({ page }) => {
await page.goto('/landing');
await expect(page.getByText('Pre-Warning Cascades')).toBeVisible();
await expect(page.getByText('Visual Timeline')).toBeVisible();
await expect(page.getByText('5 Urgency Levels')).toBeVisible();
await expect(page.getByText('Pomodoro Built-In')).toBeVisible();
await expect(page.getByText('Smart Defaults')).toBeVisible();
await expect(page.getByText('Privacy-First')).toBeVisible();
});
test('has CTA button linking to app', async ({ page }) => {
await page.goto('/landing');
const cta = page.getByRole('link', { name: /try it now/i }).first();
await expect(cta).toBeVisible();
await expect(cta).toHaveAttribute('href', '/');
});
test('footer has privacy and terms links', async ({ page }) => {
await page.goto('/landing');
await expect(page.getByRole('link', { name: 'Privacy' }).first()).toBeVisible();
await expect(page.getByRole('link', { name: 'Terms' }).first()).toBeVisible();
});
test('page renders without console errors', async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', err => errors.push(err.message));
await page.goto('/landing');
await page.waitForLoadState('networkidle');
const realErrors = errors.filter(e => !e.includes('fetch') && !e.includes('Failed'));
expect(realErrors).toHaveLength(0);
});
});

View File

@ -0,0 +1,46 @@
import { test, expect } from '@playwright/test';
test.describe('Navigation', () => {
test('dashboard loads with ChronoMind heading', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page.locator('h1').filter({ hasText: 'ChronoMind' })).toBeVisible({ timeout: 30_000 });
});
test('sidebar contains all nav links', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page.locator('h1').filter({ hasText: 'ChronoMind' })).toBeVisible({ timeout: 30_000 });
const links = ['Routines', 'History & Stats', 'Focus Mode', 'Settings'];
for (const label of links) {
await expect(page.getByRole('link', { name: label })).toBeVisible();
}
});
test('navigates to all pages without errors', async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', err => errors.push(err.message));
const routes = ['/', '/routines', '/history', '/focus', '/settings'];
for (const route of routes) {
await page.goto(route);
await page.waitForLoadState('networkidle');
}
const realErrors = errors.filter(e => !e.includes('fetch') && !e.includes('Failed'));
expect(realErrors).toHaveLength(0);
});
test('New Timer button is visible on dashboard', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page.locator('h1').filter({ hasText: 'ChronoMind' })).toBeVisible({ timeout: 30_000 });
await expect(page.getByRole('button', { name: 'New Timer' })).toBeVisible();
});
test('landing page is accessible at /landing', async ({ page }) => {
await page.goto('/landing');
await expect(page.getByText(/never be caught/i)).toBeVisible();
});
});

View File

@ -0,0 +1,36 @@
import { test, expect } from '@playwright/test';
test.describe('Privacy Page', () => {
test('renders privacy policy with all sections', async ({ page }) => {
await page.goto('/privacy');
await expect(page.getByText('Privacy Policy')).toBeVisible();
await expect(page.getByText('1. What We Collect')).toBeVisible();
await expect(page.getByText('2. Local Storage')).toBeVisible();
await expect(page.getByText('3. Analytics')).toBeVisible();
await expect(page.getByText('4. Notifications')).toBeVisible();
await expect(page.getByText('5. Cloud Sync (Future)')).toBeVisible();
await expect(page.getByText('6. Third Parties')).toBeVisible();
await expect(page.getByText('7. Contact')).toBeVisible();
});
test('has back link to dashboard', async ({ page }) => {
await page.goto('/privacy');
await expect(page.getByRole('link', { name: /back to chronomind/i })).toBeVisible();
});
});
test.describe('Terms Page', () => {
test('renders terms of service page', async ({ page }) => {
await page.goto('/terms');
await expect(page.locator('h1').first()).toBeVisible();
});
test('page renders without console errors', async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', err => errors.push(err.message));
await page.goto('/terms');
await page.waitForLoadState('networkidle');
const realErrors = errors.filter(e => !e.includes('fetch') && !e.includes('Failed'));
expect(realErrors).toHaveLength(0);
});
});

View File

@ -0,0 +1,43 @@
import { test, expect } from '@playwright/test';
test.describe('Settings — Sections', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/settings');
await page.waitForLoadState('networkidle');
await expect(page.getByText('Settings').first()).toBeVisible({ timeout: 30_000 });
});
test('appearance section shows theme toggle', async ({ page }) => {
await expect(page.getByText('Appearance')).toBeVisible();
await expect(page.getByText(/theme/i).first()).toBeVisible();
await expect(page.getByRole('button', { name: /switch to/i })).toBeVisible();
});
test('appearance section shows compact mode toggle', async ({ page }) => {
await expect(page.getByText('Compact Mode')).toBeVisible();
});
test('notifications section shows permission status', async ({ page }) => {
await expect(page.getByText('Notifications')).toBeVisible();
await expect(page.getByText('Browser Notifications')).toBeVisible();
await expect(page.getByText(/status:/i)).toBeVisible();
});
test('sound preview section shows urgency levels', async ({ page }) => {
await expect(page.getByText('Sound Preview')).toBeVisible();
await expect(page.getByText(/critical|important|standard|gentle|passive/i).first()).toBeVisible();
// Should have Preview buttons for each urgency
const previewButtons = page.getByRole('button', { name: 'Preview' });
expect(await previewButtons.count()).toBeGreaterThanOrEqual(3);
});
test('data section shows clear history option', async ({ page }) => {
await expect(page.getByText('Data')).toBeVisible();
await expect(page.getByText('Clear Completed Timers')).toBeVisible();
await expect(page.getByRole('button', { name: 'Clear' })).toBeVisible();
});
test('about section shows version', async ({ page }) => {
await expect(page.getByText(/chronomind v/i)).toBeVisible();
});
});

File diff suppressed because one or more lines are too long