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
68 changes: 68 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: CI

on:
push:
branches: ['**']
pull_request:
branches: ['**']

jobs:
unit-and-integration:
name: Unit & integration tests
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm

- name: Install dependencies
run: npm ci

- name: Run unit and integration tests with coverage
run: npm run test:coverage

- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7

e2e:
name: E2E tests
runs-on: ubuntu-latest
needs: unit-and-integration

steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm

- name: Install dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Build annotate.min.js
run: npm run build

- name: Run E2E tests
run: npm run test:e2e

- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ node_modules/
server/data/annotate.db
server/data/annotate.db-shm
server/data/annotate.db-wal
coverage/
test-results/
playwright-report/
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
23
24
41 changes: 39 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ Annotate.js/
├── relay/
│ ├── worker.js # Cloudflare Worker + Durable Object relay (WebSocket signaling)
│ └── wrangler.toml # Wrangler config for deployment
├── tests/
│ ├── unit/ # Vitest — server helper functions and REST endpoints
│ ├── integration/ # Vitest — full thread lifecycle via supertest
│ ├── e2e/ # Playwright — browser-level specs (single, BC, server-sync, access-control)
│ └── fixtures/ # Minimal HTML pages loaded by E2E tests
├── vitest.config.mjs # Vitest config — pool: forks, DATABASE_PATH=:memory:
├── playwright.config.mjs # Playwright config — headless Chromium, webServer with :memory: DB
├── .github/
│ └── workflows/
│ └── docker-publish.yml # Publishes to GHCR on v*.*.* tags
│ ├── docker-publish.yml # Publishes to GHCR on v*.*.* tags
│ └── ci.yml # CI: unit+integration → E2E (runs on every push/PR)
├── Dockerfile # Multi-stage: esbuild bundle in stage 1, lean Node runtime in stage 2
├── docker-compose.yml # image: ghcr.io/…; named volume; PORT override
├── ecosystem.config.js # PM2 config — instances: 1 (SQLite single-writer constraint)
Expand Down Expand Up @@ -156,6 +164,10 @@ are available immediately after `npm start`.
| `npm run pm2:stop` | Stop PM2 process (keeps it in pm2 list) |
| `npm run pm2:restart` | Restart PM2 process |
| `npm run pm2:logs` | Tail PM2 logs |
| `npm test` | Run unit + integration tests once |
| `npm run test:coverage` | Unit + integration with lcov coverage report |
| `npm run test:e2e` | Playwright E2E tests (headless Chromium) |
| `npm run test:all` | `test:coverage` then `test:e2e` |

### Deployment

Expand Down Expand Up @@ -491,6 +503,31 @@ See `docs/rfc-p2p-sync.md` for full architecture decisions.

## Testing

### Automated tests

```bash
npm run test:coverage # 82 unit + integration tests; server coverage report
npm run test:e2e # 15 E2E tests (headless Chromium via Playwright)
npm run test:all # both in sequence
```

| Layer | Tool | What's covered |
|---|---|---|
| **Unit** | Vitest + `pool: forks` | `rowToThread` / `threadToRow` / `rowToActivity` helpers; DB schema |
| **Integration** | Vitest + supertest | All 11 REST endpoints; thread lifecycle; ownership; admin delete; `?since=` incremental pulls |
| **E2E** | Playwright (headless Chromium) | Offline persist/reload; BroadcastChannel multi-tab; server-sync two-user; access-control gating; resolve/un-resolve; reply persistence |

**Key implementation details for tests:**
- `server/db.js` reads `DATABASE_PATH` env var; tests set `DATABASE_PATH=:memory:` for an isolated in-memory DB per process
- Vitest `pool: 'forks'` gives each test file its own process and fresh module cache
- E2E tests use unique `?<tag>=<value>` URL query params per test so `_pageUrl` differs on the server — prevents cross-test thread contamination on the shared in-memory DB
- `waitForLoadState('networkidle')` is used (not `waitForSelector`) in server-sync tests to ensure the initial `pullThreads()` HTTP request completes before `visibilitychange` is dispatched
- `page.waitForResponse` must be set up **before** the action that triggers the POST, or the response may be missed
- `annotate.js` (raw source) cannot do P2P — the P2P E2E spec is skipped unless `RUN_P2P=1` is set
- CI (`.github/workflows/ci.yml`) runs unit+integration first, then E2E on success

### Manual demo pages

Three demo pages available after `npm start`:

- **Offline-only (no server)**: `http://localhost:3000/demo/demo.html`
Expand All @@ -499,7 +536,7 @@ Three demo pages available after `npm start`:

**Note:** The server runs on port 3000. If the port is already in use (common during development when testing stops and the process remains), use `npm run kill-port` to free it, or manually kill the process with `lsof -nP -iTCP:3000 -sTCP:LISTEN` and `kill -9 <PID>`.

Test checklist for any change:
### Manual checklist for any change
- Select text → comment button appears (tooltip "Add a comment"), positioned correctly
- Add Thread → Highlight appears, ThreadCard created in sidebar
- Reload page → Threads reload, Highlights re-applied
Expand Down
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,15 @@ Annotate.js/
│ ├── routes/threads.js # Thread REST endpoints
│ ├── routes/activity.js # Activity REST endpoints
│ └── data/ # annotate.db lives here (gitignored)
├── tests/
│ ├── unit/ # Vitest — REST endpoint + db helper unit tests
│ ├── integration/ # Vitest — full HTTP lifecycle tests
│ ├── e2e/ # Playwright — browser E2E specs
│ ├── helpers/ # Shared test factories and mock relay
│ └── fixtures/ # Minimal HTML pages for E2E tests
├── vitest.config.mjs # Vitest config (pool: forks, DATABASE_PATH=:memory:)
├── playwright.config.mjs # Playwright config (Chromium, webServer)
├── .github/workflows/ci.yml # CI — unit+integration → E2E on every push/PR
├── Dockerfile # Multi-stage build → lean production image
├── docker-compose.yml # Named volume for SQLite persistence
├── ecosystem.config.js # PM2 config (instances: 1 — SQLite single-writer)
Expand All @@ -338,6 +347,10 @@ Annotate.js/
|--------|-------------|
| `npm run build` | Bundle + minify via esbuild → `annotate.min.js` (Trystero bundled in) |
| `npm start` | Start the server on port 3000 |
| `npm test` | Run unit + integration tests (Vitest) |
| `npm run test:coverage` | Unit + integration tests with lcov coverage report |
| `npm run test:e2e` | E2E tests (Playwright, headless Chromium) |
| `npm run test:all` | Unit + integration + coverage + E2E |
| `npm run kill-port` | Free port 3000 if already in use |
| `npm run pm2:start` | Start with PM2 (requires `npm i -g pm2`) |
| `npm run pm2:restart` | Restart the PM2 process |
Expand Down Expand Up @@ -399,7 +412,28 @@ Thread {

## Testing

No automated test suite. Three demo pages available after `npm start`:
### Automated test suite

```bash
npm test # unit + integration (Vitest, in-memory SQLite)
npm run test:coverage # same + lcov coverage report
npm run test:e2e # E2E browser tests (Playwright, headless Chromium)
npm run test:all # unit + integration + coverage + E2E
```

| Layer | Tool | What it covers |
|-------|------|---------------|
| **Unit** | Vitest + supertest | `rowToThread`/`threadToRow` mapping, all 11 REST endpoints |
| **Integration** | Vitest | Full thread lifecycle, ownership rules, admin delete, incremental `?since=` pull, export/import merge |
| **E2E** | Playwright (Chromium) | Select → annotate → persist; BroadcastChannel multi-tab; server-sync two-user; access control; resolve/un-resolve; replies |

Each Vitest test file runs in a forked process with `DATABASE_PATH=:memory:` so every file gets a fresh, isolated SQLite database.

CI runs on every push and pull request via `.github/workflows/ci.yml` (unit + integration with coverage upload → E2E with Playwright report on failure).

### Demo pages (manual testing)

Three demo pages available after `npm start`:

| Page | URL | Use for |
|------|-----|---------|
Expand Down
Loading
Loading