chore(web): add Dockerfile, service worker, and e2e test scaffolds
This commit is contained in:
parent
d179c4c624
commit
5453cb131a
@ -21,6 +21,10 @@ COPY web/next-env.d.ts ./next-env.d.ts
|
||||
COPY web/src/ ./src/
|
||||
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
|
||||
RUN pnpm run build
|
||||
|
||||
|
||||
43
web/e2e/auth.spec.ts
Normal file
43
web/e2e/auth.spec.ts
Normal 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
41
web/e2e/landing.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
46
web/e2e/navigation.spec.ts
Normal file
46
web/e2e/navigation.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
36
web/e2e/privacy-terms.spec.ts
Normal file
36
web/e2e/privacy-terms.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
43
web/e2e/settings-detail.spec.ts
Normal file
43
web/e2e/settings-detail.spec.ts
Normal 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
Loading…
Reference in New Issue
Block a user