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
2 changes: 1 addition & 1 deletion .claude
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,12 @@ testem.log
.DS_Store
Thumbs.db

**/.nx/**
**/.nx/**

# E2E test artifacts
e2e/node_modules
e2e/test-results
e2e/playwright-report
e2e/.test-state.json
e2e/.env.test
frontend/static/config/config.json
33 changes: 20 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- [global] Translate all German source code comments to English across e2e specs, frontend components, and utility files.

### Added

- [frontend] Self-hosted Londrina Solid Font als globale Schriftart via Fontsource; Carbon CDN-Duplikat entfernt; CSS-Variablen `--font-display` und `--font-mono` eingeführt.
- [about] Code-Blöcke verwenden jetzt ebenfalls die Londrina Solid Display-Font statt der Monospace-Font.
- [global] CSS-Variable `--letter-spacing: 0.05em` eingeführt und auf `body` sowie alle `bx--`-Elemente angewendet für bessere Lesbarkeit mit Londrina Solid.
- [frontend] `formatDate()` Hilfsfunktion in `src/lib/util/format.ts` für robuste Datumsformatierung (Fallback bei undefined/null/Invalid Date).
- [e2e] Add E2E tests for Memorandum (5 tests: startup screen, search bar, FAB modal, create folder, load preset), Todo (5 tests: empty state, FAB, menu modal, create list, add todo) and Settings (5 tests: dashboard cards, theme switch, language switch, change language, persistence).
- [e2e] Introduce Playwright E2E test infrastructure: `/e2e/` package with Chromium tests against real backend (port 61155, separate DB `tilloh-dev-e2e`); 6 spec files (auth, navigation, about, uno-sort, jokes, admin); global setup/teardown for test data; auth bypass via localStorage injection; runs locally only (no CI).
- [backend] Decorate `GET /v1/health/readyz` with `@Public()` so Playwright's webServer health check responds without a Bearer token.
- [frontend] Self-hosted Londrina Solid font as global typeface via Fontsource; removed Carbon CDN duplicate; introduced CSS variables `--font-display` and `--font-mono`.
- [about] Code blocks now also use the Londrina Solid display font instead of the monospace font.
- [global] Introduce CSS variable `--letter-spacing: 0.05em` applied to `body` and all `bx--` elements for better readability with Londrina Solid.
- [frontend] Add `formatDate()` helper in `src/lib/util/format.ts` for robust date formatting (fallback for undefined/null/Invalid Date).

### Fixed

- [about] HTML-Entities (`(`, `{`, `>` etc.) in Code-Blöcken durch echte Zeichen ersetzt — CodeSnippet rendert Plain-Text, Entities wurden wörtlich angezeigt.
- [jokes] "Invalid Date" behoben: Joke-Karte wird nur angezeigt wenn `joke.created` ein valides Datum enthält; Locale-Tag von `'DE-de'` auf korrektes BCP 47 `'de-DE'` korrigiert.
- [memorandum] Ordner-Spalten-Clipping im Fixed-Grid behoben: `gap: var(--default_padding)` auf `.content_area_fixed` gesetzt, horizontale Margins in `.link_box_fixed` entfernt.
- [hitstar] Step-Chips im Menü als nicht-interaktiv gekennzeichnet (`pointer-events: none`, `cursor: default`); Pfeil-Separatoren (`→`) zwischen den Chips eingefügt.
- [uno-sort] UnoSort-Initialisierung von `onMount` in `$effect` verschoben, sodass DOM-Refs erst nach `$initialized` gesetzt werden und kein `Cannot set properties of undefined`-Fehler auftritt.
- [about] Replace HTML entities (`(`, `{`, `>` etc.) in code blocks with actual characters — CodeSnippet renders plain text, entities were displayed literally.
- [jokes] Fix "Invalid Date": joke card is only shown when `joke.created` contains a valid date; correct locale tag from `'DE-de'` to BCP 47 `'de-DE'`.
- [memorandum] Fix folder column clipping in fixed grid: set `gap: var(--default_padding)` on `.content_area_fixed`, remove horizontal margins from `.link_box_fixed`.
- [hitstar] Mark step chips in menu as non-interactive (`pointer-events: none`, `cursor: default`); insert arrow separators (`→`) between chips.
- [uno-sort] Move UnoSort initialization from `onMount` to `$effect` so DOM refs are set only after `$initialized`, preventing `Cannot set properties of undefined` error.
- [docs] Add `docs/` folder with structured feature and shared infrastructure documentation for AI context (20 files: 1 template, 10 feature docs, 9 shared docs).
- [claude] Update commit-push skill to automatically detect and update affected docs before committing, with generic doc-to-source mapping that works across repositories.
- [hitstar] Show current game mode (Classic / Range) as small grey label top-left, only visible during an active game (hidden in the main menu).

### Changed

- [memorandum] Ordner-Erstellung über FAB + Modal statt direkter Erstellung; FAB immer sichtbar; Zero-State öffnet ebenfalls das Modal.
- [jokes] Inline-Witz-Formular durch FAB + Modal ersetzt; Seite zeigt nur noch den Zufallswitz-Bereich.
- [memorandum] Folder creation via FAB + modal instead of direct creation; FAB always visible; zero state also opens the modal.
- [jokes] Replace inline joke form with FAB + modal; page now shows only the random joke section.
- [hitstar] Remove Spotify embed (full iframe) from the reveal phase; only round tracker, abort button, flipped track card and next-round button are shown after guessing.
- [hitstar] Spotify track search now uses a single random year (1955–2025) per request instead of a fixed range to improve year distribution across decades.
- [hitstar] Add `genre:pop` filter to Spotify search query to increase the share of well-known tracks.
Expand Down Expand Up @@ -75,7 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [global] Reduced GlobalMenu font sizes on mobile for better fit.
- [hitstar] Redesigned GUESSING/REVEAL UI: question headline + small danger Exit icon button in top bar, round progress tracker (gray/green/red bars), year input and submit icon button inline, submit icon centered via flex override.
- [global] Claude Code `/commit-push` Skill für automatisierten Commit-und-Push-Workflow.
- [global] Post-Commit Hook erweitert mit Unicode-Gitmojis und zusätzlichen Keywords (test, style, update, improve, move, breaking, access, database, responsive, animation, i18n, clean).
- [global] Extend post-commit hook with Unicode gitmojis and additional keywords (test, style, update, improve, move, breaking, access, database, responsive, animation, i18n, clean).
- [frontend] Added `viewport-fit=cover` to enable iPhone safe-area support (Notch/Home-Indicator) across all pages.
- [memorandum] Empty state redesigned as Carbon Tile cards with title, description, and CTA buttons.
- [memorandum] FAB add-folder button is now `kind="primary"` and hidden in empty state.
Expand Down Expand Up @@ -113,7 +120,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- [home] Navigation tiles show icon-only on phone; `JokeOfTheDay` section is now prominently displayed with a Carbon Tile card.
- [global] `GlobalMenu` footer shows GitHub/Stadtwerk buttons below "Entwickelt von..." text on phone as social buttons.
- [global] `GlobalMenu` footer shows GitHub/Stadtwerk buttons below "Developed by..." text on phone as social buttons.

### Changed

Expand Down
36 changes: 36 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,31 @@ cd backend && nx run-many -t test # Jest tests
cd frontend && nx run-many -t test # Vitest tests
```

### E2E Tests (`/e2e/`)
E2E tests run **locally only** (not in CI). They start backend (port 61155) and frontend (port 5173) automatically via Playwright `webServer`.

**Prerequisites:**
- MongoDB running locally (`cd backend && npm run start:db`)
- `e2e/.env.test` exists (copy from `.env.test.example`)

```bash
npm run e2e:install # One-time: npm install + download Chromium
npm run e2e:setup # One-time: copy .env.test.example → .env.test

npm run e2e # Run tests headless
npm run e2e:headed # Run tests with visible browser
npm run e2e:ui # Playwright UI (interactive mode)
```

**Configuration** (`e2e/.env.test`):
- `E2E_ADMIN_IDENTIFIER` – must match `ADMIN_IDENTIFIER` in `backend/.env`
- `E2E_MONGO_DB_URL` – separate database `tilloh-dev-e2e` (no conflict with dev data)
- `E2E_BACKEND_URL` – port 61155 (no conflict with dev backend on 61154)

**Auth strategy**: Tests inject the test identifier via `localStorage.setItem('identifier', id)` using `addInitScript()` before page load. This bypasses the GlobalLogin gate without UI interaction.

**Global Setup/Teardown**: Before tests, a test identifier and a seed joke are created in the E2E database. After tests, they are cleaned up.

## Architecture

```
Expand All @@ -48,6 +73,7 @@ cd frontend && nx run-many -t test # Vitest tests
├── backend/ # NestJS API (Fastify, Mongoose, Socket.io)
│ ├── apps/tilloh-dev/ # Main application entry
│ └── libs/ # Feature modules (NX libraries)
├── e2e/ # Playwright E2E tests (local only, Chromium)
├── docker-compose.yaml # Docker orchestration
└── git-hooks/ # Pre/post-commit automation
```
Expand Down Expand Up @@ -115,6 +141,16 @@ Structured docs live in `docs/`. Use these as context when working on or extendi

## Coding Best Practices

### Language Policy
All source code **must be in English only**. This applies to:
- **Code**: Variable names, function names, class names, type names
- **Comments**: Inline comments, JSDoc, block comments
- **CHANGELOG entries**: All entries in `CHANGELOG.md`
- **Commit messages**: Commit message text (excluding gitmoji)
- **Test descriptions**: `test()` and `describe()` labels in spec files

The only exceptions are i18n translation strings (`de.json` / `en.json`) and user-facing UI text managed through the i18n system.

### Frontend Organization
- **API calls**: Always place in `/frontend/src/lib/api/`
- **Type definitions**: Always place in `/frontend/src/lib/types/`
Expand Down
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,54 @@ After starting all services contained in docker-compose file the first user must

Use a tool like Postman or do a POST http://localhost:61154/v1/identifiers request the way you like. It is important to provide an Authrization: Bearer <ADMIN_IDENTIFIER> header in the request to authorize. After that you will get an identifier id that can be used for further usage in the application user interface login screen.

## E2E Tests

E2E tests run locally with [Playwright](https://playwright.dev/) against a real backend + MongoDB. Backend (port 61155) and frontend (port 5173) are started **automatically** — no manual startup required.

### Setup (one-time)

```bash
# 1. Start MongoDB
cd backend && npm run start:db

# 2. Install E2E dependencies and Chromium
npm run e2e:install

# 3. Create and fill .env.test
npm run e2e:setup
# Open e2e/.env.test and set E2E_ADMIN_IDENTIFIER (= ADMIN_IDENTIFIER from backend/.env)
```

### Running tests

```bash
npm run e2e # Headless (fast)
npm run e2e:headed # With visible browser
npm run e2e:ui # Playwright UI (interactive, with timeline and traces)
```

### Structure

```
e2e/
playwright.config.ts # Config: Chromium, webServer, global setup/teardown
.env.test.example # Template for local config (committed)
global-setup.ts # Creates test identifier + seed joke in DB
global-teardown.ts # Cleans up test data
fixtures/
authenticated.fixture.ts # localStorage injection for auth bypass
helpers/
api.ts # HTTP helpers for setup/teardown
constants.ts # URLs, identifiers
tests/
auth/login.spec.ts # Login gate and login flow
navigation/home.spec.ts # Header, nav grid, tile navigation
features/about.spec.ts # Static content, i18n DE/EN
features/uno-sort.spec.ts # Card drawing, reset (no backend)
features/jokes.spec.ts # Random joke, FAB modal
features/admin.spec.ts # Two-step auth, dashboard
```

## Development

Before committing changes it is necessary to fix all linting errors. After cloning this repository, `npm install` will automatically install a pre-commit hook that will run the linter before committing changes!
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Controller, Get } from '@nestjs/common';
import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { Public } from '@backend/util';
import {
HealthCheckResult,
HealthCheckService,
Expand Down Expand Up @@ -48,6 +49,7 @@ export function healthControllerFactory() {
return metrics.replace(/\n$/, '');
}

@Public()
@Get('readyz')
@ApiOkResponse({
description: 'Successful response',
Expand Down
22 changes: 22 additions & 0 deletions e2e/.env.test.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# E2E Test Environment Variables
# Copy this file to .env.test and fill in your values

# Admin identifier for the backend (must match ADMIN_IDENTIFIER in backend/.env)
E2E_ADMIN_IDENTIFIER=your-admin-identifier-here

# Backend URL for E2E tests (uses separate port to avoid conflicts with dev backend)
E2E_BACKEND_URL=http://localhost:61155/v1

# Frontend URL for E2E tests
E2E_FRONTEND_URL=http://localhost:5173

# MongoDB connection for E2E (separate database to avoid polluting dev data)
E2E_MONGO_DB_URL=mongodb://localhost/tilloh-dev-e2e

# Spotify credentials (required for backend startup)
E2E_SPOTIFY_CLIENT_ID=your-spotify-client-id
E2E_SPOTIFY_CLIENT_SECRET=your-spotify-client-secret

# OCR Space API (required for backend startup)
E2E_OCR_SPACE_URL=https://api.ocr.space/parse/image
E2E_OCR_SPACE_API_KEY=your-ocr-api-key
30 changes: 30 additions & 0 deletions e2e/fixtures/authenticated.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as fs from 'fs';
import { test as base, type Page } from '@playwright/test';
import { TEST_STATE_FILE } from '../helpers/constants';

type AuthenticatedFixtures = {
authenticatedPage: Page;
};

/**
* Custom fixture that injects a valid identifier into localStorage before
* navigating to the app, bypassing the GlobalLogin gate.
*/
export const test = base.extend<AuthenticatedFixtures>({
authenticatedPage: async ({ page }, use) => {
const state = JSON.parse(fs.readFileSync(TEST_STATE_FILE, 'utf-8'));
const testIdentifierId: string = state.testIdentifierId;

// Set identifier in localStorage before page load so the layout reads it
await page.addInitScript(
(id: string) => {
localStorage.setItem('identifier', id);
},
testIdentifierId,
);

await use(page);
},
});

export { expect } from '@playwright/test';
39 changes: 39 additions & 0 deletions e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as fs from 'fs';
import {
createTestIdentifier,
createTestJoke,
waitForBackendReady,
} from './helpers/api';
import {
BACKEND_URL,
FRONTEND_CONFIG_FILE,
TEST_IDENTIFIER_NAME,
TEST_STATE_FILE,
} from './helpers/constants';

export default async function globalSetup() {
// Point frontend at the E2E backend port
fs.writeFileSync(
FRONTEND_CONFIG_FILE,
JSON.stringify({ backendUrl: BACKEND_URL }),
);
console.log(`[E2E] Frontend config written: backendUrl=${BACKEND_URL}`);

console.log('\n[E2E] Waiting for backend to be ready...');
await waitForBackendReady();
console.log('[E2E] Backend is ready.');

console.log('[E2E] Creating test identifier...');
const testIdentifierId = await createTestIdentifier(TEST_IDENTIFIER_NAME);
console.log(`[E2E] Test identifier created: ${testIdentifierId}`);

console.log('[E2E] Creating seed joke...');
const testJokeId = await createTestJoke(
'E2E test joke: Why can programmers never sleep? Because they have too many loops.',
);
console.log(`[E2E] Seed joke created: ${testJokeId}`);

const state = { testIdentifierId, testJokeId };
fs.writeFileSync(TEST_STATE_FILE, JSON.stringify(state, null, 2));
console.log('[E2E] Test state saved.');
}
32 changes: 32 additions & 0 deletions e2e/global-teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as fs from 'fs';
import { deleteTestIdentifier, deleteTestJoke } from './helpers/api';
import { FRONTEND_CONFIG_FILE, TEST_STATE_FILE } from './helpers/constants';

export default async function globalTeardown() {
if (!fs.existsSync(TEST_STATE_FILE)) {
console.log('[E2E] No test state found, skipping teardown.');
return;
}

const state = JSON.parse(fs.readFileSync(TEST_STATE_FILE, 'utf-8'));
const { testIdentifierId, testJokeId } = state;

if (testIdentifierId) {
console.log(`[E2E] Deleting test identifier: ${testIdentifierId}`);
await deleteTestIdentifier(testIdentifierId);
}

if (testJokeId) {
console.log(`[E2E] Deleting seed joke: ${testJokeId}`);
await deleteTestJoke(testJokeId);
}

fs.unlinkSync(TEST_STATE_FILE);
console.log('[E2E] Test state cleaned up.');

// Remove frontend config
if (fs.existsSync(FRONTEND_CONFIG_FILE)) {
fs.unlinkSync(FRONTEND_CONFIG_FILE);
console.log('[E2E] Frontend config removed.');
}
}
Loading
Loading