Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 35 additions & 12 deletions cypress/e2e/landing.cy.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
// This test suite is for the landing page
describe('Landing Page', () => {
// This hook runs before each test in the suite
beforeEach(() => {
// Visit the landing page
cy.visit('http://localhost:3000');
});

afterEach(() => {
// Accessibility check
cy.a11yCheck();
});

// This test checks that the page renders correctly
it('should render page', () => {
cy.visit('http://localhost:3000');
// Check that the profile picture exists on the page
cy.get('[data-testid="profile_pic"]').should('exist');
});

it('should show cookie snackbar on first load and not after accepting', () => {
const hydrationErrorPattern = /hydration|did not match|content does not match/i;
cy.on('window:before:load', (win) => {
cy.spy(win.console, 'error').as('consoleError');
});
cy.visit('http://localhost:3000');

// The snackbar should be visible on first load
cy.get('.MuiSnackbar-root').should('exist').and('be.visible');
Expand All @@ -36,8 +28,39 @@ describe('Landing Page', () => {

// The snackbar should not appear again
cy.get('.MuiSnackbar-root').should('not.exist');
});

// Assert no hydration errors in the browser console
cy.get('@consoleError').should('not.have.been.calledWithMatch', hydrationErrorPattern);
it('initialises without console errors or uncaught exceptions', () => {
const uncaught: Error[] = [];
cy.on('window:before:load', (win) => {
cy.spy(win.console, 'error').as('consoleError');

win.addEventListener('error', (event) => {
uncaught.push(event.error ?? new Error(event.message));
});
win.addEventListener('unhandledrejection', (event) => {
const reason = (event as PromiseRejectionEvent).reason;
uncaught.push(reason instanceof Error ? reason : new Error(String(reason)));
});
});

cy.visit('http://localhost:3000');
cy.get('[data-testid="profile_pic"]').should('exist');

cy.get('@consoleError').then((spy: unknown) => {
const calls = (spy as sinon.SinonSpy).getCalls();
const messages = calls.map((c) =>
c.args.map((a: unknown) => (a instanceof Error ? a.message : String(a))).join(' '),
);

expect(messages, `unexpected console.error calls:\n${messages.join('\n')}`).to.have.lengthOf(0);
});

cy.then(() => {
expect(
uncaught,
`unexpected uncaught errors:\n${uncaught.map((e) => e.message).join('\n')}`,
).to.have.lengthOf(0);
});
});
});
13 changes: 8 additions & 5 deletions docs/architecture/configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Configs manage environment variables, service integrations, and global settings
- `.env`: Environment variables (API keys, secrets)
- [`next.config.js`](../../next.config.js): Next.js build/runtime config
- [`sentry.client.config.ts`](../../sentry.client.config.ts), [`sentry.server.config.ts`](../../sentry.server.config.ts), [`sentry.edge.config.ts`](../../sentry.edge.config.ts): Sentry error tracking
- [`src/instrumentation.ts`](../../src/instrumentation.ts), [`src/instrumentation-client.ts`](../../src/instrumentation-client.ts): Next.js Instrumentation hooks for Sentry

## Usage Examples

Expand Down Expand Up @@ -43,13 +44,15 @@ const apiKey = process.env.NEXT_PUBLIC_API_KEY;

### Sentry Configuration

Sentry is configured via the repository's `sentry.*.config.ts` files. Use Sentry's Next.js SDK initialization patterns in those files; the app uses the standard `@sentry/nextjs` entrypoints. Example usage in application code:
Sentry is initialized via three `sentry.*.config.ts` files at the project root and two Next.js Instrumentation hooks:

```ts
import * as Sentry from '@sentry/nextjs';
- [`sentry.client.config.ts`](../../sentry.client.config.ts) — Client-side initialization, including session replay and console error capture via `Sentry.captureConsoleIntegration`
- [`sentry.server.config.ts`](../../sentry.server.config.ts) — Server-side initialization
- [`sentry.edge.config.ts`](../../sentry.edge.config.ts) — Edge runtime initialization
- [`src/instrumentation.ts`](../../src/instrumentation.ts) — Next.js `register()` hook that loads the server or edge Sentry config based on the `NEXT_RUNTIME` environment variable; also exports `onRequestError = Sentry.captureRequestError` for automatic request error capture
- [`src/instrumentation-client.ts`](../../src/instrumentation-client.ts) — Exports `onRouterTransitionStart = Sentry.captureRouterTransitionStart` for client-side router transition tracking

Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN });
```
All `Sentry.*` integrations are imported directly from `@sentry/nextjs`.

## Integration & Relationships

Expand Down
5 changes: 1 addition & 4 deletions docs/architecture/pwa.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,7 @@ export default function manifest(): MetadataRoute.Manifest {

### Service Worker Lifecycle

This project includes a manual service worker implementation at `public/sw.js` and registers it from the client. The app registers the service worker in two places:

- `src/components/ServiceWorkerRegister.tsx` — a small client component that calls `navigator.serviceWorker.register('/sw.js')` inside a `useEffect`.
- `src/app/page.tsx` — the home page also calls `navigator.serviceWorker.register('/sw.js')` during its initial `useEffect` (this is a defensive duplicate to ensure registration on client navigations).
This project includes a manual service worker implementation at `public/sw.js` and registers it from the client. The app registers the service worker via `src/components/ServiceWorkerRegister.tsx` — a small client component that calls `navigator.serviceWorker.register('/sw.js')` inside a `useEffect`, with linear backoff retry logic on failure.

The service worker uses a cache-first / stale-while-revalidate strategy for static assets and network-first for navigation requests. See `public/sw.js` for the exact implementation.

Expand Down
27 changes: 3 additions & 24 deletions docs/architecture/service-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,11 @@ This file documents the service worker implementation used by the site.

## How the app registers the service worker

The app registers the SW from two locations:
The app registers the SW from [src/components/ServiceWorkerRegister.tsx](../../src/components/ServiceWorkerRegister.tsx), a client component included in the root layout.

**ServiceWorkerRegister component** ([src/components/ServiceWorkerRegister.tsx](../../src/components/ServiceWorkerRegister.tsx)):
The component calls `navigator.serviceWorker.register('/sw.js')` inside a `useEffect`. On failure it retries up to `MAX_SW_RETRIES` (3) times with linear backoff starting at `INITIAL_RETRY_DELAY` (1000 ms), clearing any pending retry on unmount.

```tsx
import { useEffect } from 'react';

export default function ServiceWorkerRegister() {
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then((reg) => console.log('Service Worker registered with scope', reg.scope))
.catch((err) => console.error('Service Worker registration failed:', err));
}
}, []);

return null;
}
```

**Home page** ([src/app/page.tsx](../../src/app/page.tsx)) also registers the SW within its `useEffect` as a defensive measure for client-side navigations.

Both registration points use identical registration logic to ensure the service worker is registered regardless of entry point.

Implementation: [ServiceWorkerRegister.tsx](../../src/components/ServiceWorkerRegister.tsx), [page.tsx](../../src/app/page.tsx)
Implementation: [ServiceWorkerRegister.tsx](../../src/components/ServiceWorkerRegister.tsx)

## Customizing caching

Expand Down
Loading
Loading