Compare commits

..
18 Commits
Author SHA1 Message Date
github-actions[bot] b8a87d1626 chore: bump version to 0.3.3 2026-03-07 07:04:44 +00:00
Frederic CabassutandClaude Opus 4.6 b7b9bca29a fix: update docker-compose only after Docker image is pushed
Move docker-compose.yml update from release.sh (pre-build) to a
post-release job in both bump.yml and release.yml. This ensures the
image tag in docker-compose only appears on main after the image
exists on ghcr.io, preventing Dokploy from trying to pull a
non-existent image.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:04:18 +01:00
github-actions[bot] bd5819da61 chore: bump version to 0.3.2 2026-03-07 06:56:57 +00:00
Frederic CabassutandClaude Opus 4.6 96e0212d55 fix: include tags in sync pull response and update API spec
- GET /api/sync now attaches tags to notes (matching GET /api/notes
  and GET /api/search behavior)
- Add trashedAt field to Note schema in OpenAPI spec
- Add optional id field to NoteInput schema
- Add 400 error response for POST /api/sync
- Add response schema for GET attachments list
- Regenerate API.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:52:36 +01:00
Frederic CabassutandClaude Opus 4.6 5cccaa911a docs: update FEATURES.md, DEPLOYMENT.md, and CLAUDE.md
- FEATURES.md: add rich text editor section (Tiptap, formatting toolbar,
  raw markdown mode) and expand text formatting list
- DEPLOYMENT.md: fix docker-compose example to use image from ghcr.io
  instead of local build
- CLAUDE.md: add documentation maintenance instructions, update CI/CD
  section to reflect consolidated bump.yml workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:50:10 +01:00
github-actions[bot] 7d2c545ac5 chore: bump version to 0.3.1 2026-03-07 06:45:06 +00:00
Frederic CabassutandClaude Opus 4.6 ba4f37d413 fix: consolidate release pipeline into bump workflow
- Update release.sh to also bump docker-compose.yml image tag in the
  same commit, keeping the tagged commit in sync
- Add CI and Docker release jobs to bump.yml so it's self-contained
  (GITHUB_TOKEN pushes don't trigger other workflows)
- Remove "Pin docker-compose" post-step from release.yml since it's
  now handled by release.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:44:15 +01:00
github-actions[bot] 6f7560abd1 chore: bump version to 0.3.0 2026-03-07 06:08:01 +00:00
Frederic CabassutandClaude Opus 4.6 5e46545dc6 feat: replace pretzel emoji with favicon across the app
Use the new favicon SVG in the header, login, setup, empty state,
and error pages. Keep the pretzel emoji in the footer tagline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:05:59 +01:00
Frederic CabassutandClaude Opus 4.6 f459f39ae6 feat: add proper favicon set from RealFaviconGenerator
Replace inline emoji favicon with full cross-platform favicon support
including SVG, PNG, ICO, Apple touch icon, and PWA manifest icons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:05:59 +01:00
Frederic CabassutandClaude Opus 4.6 b71bdf63e6 rename "Horizontal rule" to "Divider" in formatting toolbar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:05:59 +01:00
github-actions[bot] 14f7c1deaa chore: bump version to 0.2.3 2026-03-07 05:32:22 +00:00
Frederic CabassutandClaude Opus 4.6 56b0510789 fix: update docker-compose image tag after CI builds the image
Move docker-compose version pinning from release script to the release
workflow so it runs after the image is pushed to GHCR. Dokploy will
always find the referenced image.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:48:28 +01:00
Frederic Cabassut bd9762600e chore: bump version to 0.2.2 2026-03-06 23:46:32 +01:00
Frederic CabassutandClaude Opus 4.6 f1ee1744e7 chore: pin docker-compose image to version tag on release
Release script now updates docker-compose.yml with the exact version
tag so Dokploy always pulls the correct image.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:45:47 +01:00
Frederic Cabassut 40e32ae8fc chore: bump version to 0.2.1 2026-03-06 23:38:13 +01:00
Frederic CabassutandClaude Opus 4.6 c1c4fe5056 chore: switch to GHCR image in docker-compose, remove dokploy webhook
- docker-compose.yml now uses pre-built ghcr.io image instead of build
- ORIGIN is configurable via env var for deployment
- Remove deploy job from release workflow (Dokploy pulls compose from repo)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:35:08 +01:00
Frederic CabassutandClaude Opus 4.6 e54e706b65 fix: make E2E tests resilient to shared DB in parallel execution
- Add noteCard() helper to target specific cards by title instead of
  generic getByTestId('note-card') which fails with multiple matches
- Scope action button clicks (trash, pin, archive) within their card
- Scope tag-chip filter clicks to the tag-filter container
- Wait for networkidle after auth to ensure full hydration
- Remove "empty notes list" test incompatible with shared DB
- Use toHaveCount(0) instead of not.toBeVisible for search empty state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:32:42 +01:00
31 changed files with 265 additions and 58 deletions
+86 -1
View File
@@ -18,6 +18,8 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
tag: ${{ steps.release-tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
@@ -34,4 +36,87 @@ jobs:
node-version: 22
- name: Run release script
run: bash scripts/release.sh ${{ github.event.inputs.bump }}
env:
BUMP_TYPE: ${{ github.event.inputs.bump }}
run: bash scripts/release.sh "$BUMP_TYPE"
- name: Output new tag
id: release-tag
run: echo "tag=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT"
ci:
needs: bump
uses: ./.github/workflows/ci.yml
release:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: [bump, ci]
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.bump.outputs.tag }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}},value=${{ needs.bump.outputs.tag }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.bump.outputs.tag }}
type=raw,value=latest
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.bump.outputs.tag }}
generate_release_notes: true
pin-docker-compose:
name: Pin docker-compose to released version
runs-on: ubuntu-latest
needs: [bump, release]
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update docker-compose.yml
env:
TAG: ${{ needs.bump.outputs.tag }}
run: |
VERSION="${TAG#v}"
sed -i "s|image: ghcr.io/bretzel-app/crumbs:.*|image: ghcr.io/bretzel-app/crumbs:${VERSION}|" docker-compose.yml
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add docker-compose.yml
git diff --cached --quiet && exit 0
git commit -m "chore: pin docker-compose to ${TAG} [skip ci]"
git push origin main
+21 -4
View File
@@ -55,10 +55,27 @@ jobs:
with:
generate_release_notes: true
deploy:
name: Deploy
pin-docker-compose:
name: Pin docker-compose to released version
runs-on: ubuntu-latest
needs: release
permissions:
contents: write
steps:
- name: Trigger Dokploy redeploy
run: curl -X POST "${{ secrets.DOKPLOY_WEBHOOK_URL }}"
- uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update docker-compose.yml
env:
TAG: ${{ github.ref_name }}
run: |
VERSION="${TAG#v}"
sed -i "s|image: ghcr.io/bretzel-app/crumbs:.*|image: ghcr.io/bretzel-app/crumbs:${VERSION}|" docker-compose.yml
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add docker-compose.yml
git diff --cached --quiet && exit 0
git commit -m "chore: pin docker-compose to ${TAG} [skip ci]"
git push origin main
+17 -2
View File
@@ -135,10 +135,14 @@ GitHub Actions with two workflows:
4. **E2E Tests** — Playwright with Chromium (depends on step 3, uploads report on failure)
5. **Docker Build** — validates the Docker image builds (depends on step 3)
**Release** (`.github/workflows/release.yml`) — runs on `v*` tags:
- Runs full CI pipeline, then builds + pushes Docker image to `ghcr.io` (GitHub Container Registry)
**Bump & Release** (`.github/workflows/bump.yml`) — workflow_dispatch (primary release method):
- Runs `scripts/release.sh` to bump version in `package.json` + `docker-compose.yml`, tag, and push
- Runs full CI pipeline, then builds + pushes Docker image to `ghcr.io`
- Creates a GitHub Release with auto-generated notes
**Release** (`.github/workflows/release.yml`) — runs on `v*` tags (fallback for manual/local tag pushes):
- Same as above: CI pipeline, Docker build/push, GitHub Release
**Docker**: Multi-stage Dockerfile (node:22-slim), exposes port 3000, persists data to `/data` volume.
## Testing philosophy: BDD/TDD
@@ -188,3 +192,14 @@ E2E tests follow Cucumber BDD conventions via comments. Follow the [Better Gherk
```
**Resilience test** — ask: *"Will this comment need to change if the UI implementation changes?"* If yes, rewrite it to remove implementation details. The Playwright code underneath handles the *how*; the comments describe the *what*.
## Documentation
Keep docs in `docs/` up to date when making changes. If a feature, API endpoint, deployment config, or architectural decision changes, update the relevant doc file in the same commit or PR:
| Doc | Covers |
|-----|--------|
| `docs/FEATURES.md` | User-facing feature list |
| `docs/ARCHITECTURE.md` | System design, data flow, schema, tech stack |
| `docs/DEPLOYMENT.md` | Docker, env vars, reverse proxy, backups |
| `docs/openapi.yaml` | API spec (run `pnpm docs:api` to regenerate `docs/API.md`) |
+2 -2
View File
@@ -1,6 +1,6 @@
services:
crumbs:
build: .
image: ghcr.io/bretzel-app/crumbs:0.3.2
container_name: crumbs
ports:
- "3000:3000"
@@ -10,7 +10,7 @@ services:
- NODE_ENV=production
- DATA_DIR=/data
- DATABASE_URL=/data/crumbs.db
- ORIGIN=http://localhost:3000
- ORIGIN=${ORIGIN:-http://localhost:3000}
restart: unless-stopped
volumes:
+24
View File
@@ -103,6 +103,7 @@ List notes
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -125,6 +126,7 @@ Create a note
```json
{
"id": "uuid",
"title": "title",
"content": "content",
"color": "default",
@@ -147,6 +149,7 @@ Create a note
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -181,6 +184,7 @@ Get a note
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -212,6 +216,7 @@ Partial update — only include changed fields.
```json
{
"id": "uuid",
"title": "title",
"content": "content",
"color": "default",
@@ -234,6 +239,7 @@ Partial update — only include changed fields.
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -288,6 +294,19 @@ Without `attachmentId`, lists all attachments. With `attachmentId`, returns the
**Response:** `200`
```json
[
{
"id": "id",
"noteId": "noteId",
"filename": "filename",
"mimeType": "mimeType",
"size": 1,
"createdAt": "2025-01-01T00:00:00.000Z"
}
]
```
---
### `POST /api/notes/{id}/attachments`
@@ -366,6 +385,7 @@ Searches across note titles, content, and tags. Excludes trashed notes.
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -423,6 +443,7 @@ Pull changes since timestamp
"pinned": true,
"archived": true,
"trashed": true,
"trashedAt": "2025-01-01T00:00:00.000Z",
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
@@ -451,6 +472,7 @@ Push local changes
"operation": "create",
"timestamp": 1,
"data": {
"id": "uuid",
"title": "title",
"content": "content",
"color": "default",
@@ -473,4 +495,6 @@ Push local changes
}
```
**Errors:** `400` Invalid changes payload
---
+1 -1
View File
@@ -16,7 +16,7 @@ Open http://localhost:3000 and set your password.
# docker-compose.yml
services:
crumbs:
build: .
image: ghcr.io/bretzel-app/crumbs:latest
ports:
- "8080:3000" # Change external port
volumes:
+15 -4
View File
@@ -8,16 +8,27 @@
- Content supports full Markdown syntax
- Auto-save on editor close
### Markdown Support
- **Bold** (`**text**`)
- *Italic* (`*text*`)
### Rich Text Editor
- WYSIWYG editing powered by Tiptap
- Formatting toolbar with visual controls
- Raw Markdown mode toggle for direct editing
- Undo/Redo with keyboard shortcuts
### Text Formatting
- **Bold** (`**text**` / Ctrl+B)
- *Italic* (`*text*` / Ctrl+I)
- Underline (Ctrl+U)
- ~~Strikethrough~~ (`~~text~~` / Ctrl+Shift+X)
- Headings (`# H1` through `###### H6`)
- Blockquotes (`> quote`)
- Unordered lists (`- item`)
- Ordered lists (`1. item`)
- Code blocks (triple backtick)
- Inline code (single backtick)
- Tables
- Links (auto-linkified URLs)
- Links — insert, edit, and remove with URL input
- Dividers (horizontal rules)
- Text alignment (left, center, right, justify)
- Line breaks
### Checklists / Task Lists
+17 -1
View File
@@ -187,7 +187,13 @@ paths:
type: string
responses:
'200':
description: Attachment list or file content
description: Attachment list (without attachmentId) or file content (with attachmentId)
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Attachment'
post:
summary: Upload an image attachment
tags: [Attachments]
@@ -306,6 +312,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Success'
'400':
description: Invalid changes payload
components:
securitySchemes:
@@ -340,6 +348,10 @@ components:
type: boolean
trashed:
type: boolean
trashedAt:
type: string
format: date-time
nullable: true
checklistMode:
type: boolean
sortOrder:
@@ -360,6 +372,10 @@ components:
NoteInput:
type: object
properties:
id:
type: string
format: uuid
description: Optional — generated if omitted
title:
type: string
content:
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "crumbs",
"private": true,
"version": "0.2.0",
"version": "0.3.3",
"type": "module",
"scripts": {
"dev": "vite dev",
+6 -4
View File
@@ -6,10 +6,12 @@
<meta name="theme-color" content="#f0e6d3" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🥨</text></svg>"
/>
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Crumbs" />
<link rel="manifest" href="/site.webmanifest" />
<title>Crumbs</title>
%sveltekit.head%
</head>
+1 -1
View File
@@ -344,7 +344,7 @@
data-testid="format-hr"
>
<Minus size={iconSize} />
<span>Horizontal rule</span>
<span>Divider</span>
</button>
<div class="my-1 h-px bg-[var(--border-subtle)]"></div>
<button
+1
View File
@@ -28,6 +28,7 @@
</button>
<div class="flex items-center gap-2">
<img src="/favicon.svg" alt="" class="h-8 w-8" />
<h1 class="font-['Press_Start_2P'] text-lg text-[var(--primary)]">Crumbs</h1>
</div>
+1 -1
View File
@@ -77,7 +77,7 @@
{#if $notesLoaded && $pinnedNotes.length === 0 && $unpinnedNotes.length === 0}
<div class="flex flex-col items-center justify-center py-20 text-[var(--text-muted)]">
<span class="mb-4 text-7xl">🥨</span>
<img src="/favicon.svg" alt="" class="mb-4 h-18 w-18" />
<p class="font-['Press_Start_2P'] text-xs">
{#if $currentFilter === 'trashed'}
No crumbs in trash
+1 -1
View File
@@ -35,7 +35,7 @@
<div class="w-full max-w-sm rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-8 shadow-[var(--card-shadow)]">
<div class="mb-6 text-center">
<span class="text-5xl">🥨</span>
<img src="/favicon.svg" alt="Crumbs" class="mx-auto h-12 w-12" />
<h1 class="font-['Press_Start_2P'] text-xl text-[var(--primary)]">Crumbs</h1>
<p class="mt-2 text-sm text-[var(--text-muted)]">Enter your password to continue</p>
</div>
+1 -1
View File
@@ -47,7 +47,7 @@
<div class="w-full max-w-sm rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-8 shadow-[var(--card-shadow)]">
<div class="mb-6 text-center">
<span class="text-5xl">🥨</span>
<img src="/favicon.svg" alt="Crumbs" class="mx-auto h-12 w-12" />
<h1 class="font-['Press_Start_2P'] text-xl text-[var(--primary)]">Welcome to Crumbs</h1>
<p class="mt-2 text-sm text-[var(--text-muted)]">Set up your password to get started</p>
</div>
+1 -1
View File
@@ -8,7 +8,7 @@
<div class="flex min-h-screen items-center justify-center bg-[var(--bg-base)]">
<div class="w-full max-w-sm rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-8 text-center">
<span class="text-5xl">🥨</span>
<img src="/favicon.svg" alt="Crumbs" class="mx-auto h-12 w-12" />
<h1 class="mt-4 font-['Press_Start_2P'] text-xl text-[var(--primary)]">
{page.status}
</h1>
+9 -1
View File
@@ -1,6 +1,7 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types.js';
import { processSyncPush, getChangesSince } from '$lib/sync/server.js';
import { fetchTagsForNotes } from '$lib/server/tags.js';
export const POST: RequestHandler = async ({ request }) => {
const { changes } = await request.json();
@@ -16,5 +17,12 @@ export const POST: RequestHandler = async ({ request }) => {
export const GET: RequestHandler = async ({ url }) => {
const since = parseInt(url.searchParams.get('since') || '0', 10);
const changes = await getChangesSince(since);
return json(changes);
const tagMap = fetchTagsForNotes(changes.map((n) => n.id));
const changesWithTags = changes.map((note) => ({
...note,
tags: tagMap.get(note.id) ?? []
}));
return json(changesWithTags);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.4 MiB

+21
View File
@@ -0,0 +1,21 @@
{
"name": "Crumbs by Bretzel",
"short_name": "Crumbs",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#f9f4ea",
"background_color": "#f9f4ea",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

+4 -4
View File
@@ -1,4 +1,4 @@
import { test, expect } from './helpers/fixtures.js';
import { test, expect, noteCard } from './helpers/fixtures.js';
test.describe('Checklist', () => {
test('Scenario: Checklist UI replaces editor when checklist mode is enabled', async ({ authenticatedPage: page }) => {
@@ -27,7 +27,7 @@ test.describe('Checklist', () => {
await expect(page.getByText('Shopping List')).toBeVisible();
// When the user reopens the note
await page.getByTestId('note-card').click();
await noteCard(page, 'Shopping List').click();
// Then the checklist is displayed with the saved item
await expect(page.getByTestId('checklist')).toBeVisible();
@@ -43,12 +43,12 @@ test.describe('Checklist', () => {
await page.getByTestId('close-editor-btn').click();
// When the user reopens the note and checks the item
await page.getByTestId('note-card').click();
await noteCard(page, 'Tasks').click();
await page.getByTestId('checklist-checkbox').first().check();
await page.getByTestId('close-editor-btn').click();
// Then reopening the note shows the item is still checked
await page.getByTestId('note-card').click();
await noteCard(page, 'Tasks').click();
await expect(page.getByTestId('checklist-checkbox').first()).toBeChecked();
});
+2 -2
View File
@@ -1,4 +1,4 @@
import { test, expect } from './helpers/fixtures.js';
import { test, expect, noteCard } from './helpers/fixtures.js';
import type { Page, Locator } from '@playwright/test';
/** Run a TipTap command chain via the exposed editor instance on the DOM element */
@@ -94,7 +94,7 @@ test.describe('Rich text formatting', () => {
await page.getByTestId('close-editor-btn').click();
// When the user reopens the note
await page.getByTestId('note-card').click();
await noteCard(page, 'Rich Text Note').click();
// Then the content is rendered as rich text in the TipTap editor
await expect(page.getByTestId('tiptap-editor')).toBeVisible();
+10 -1
View File
@@ -1,7 +1,12 @@
import { test as base, expect, type Page } from '@playwright/test';
import { test as base, expect, type Page, type Locator } from '@playwright/test';
const TEST_PASSWORD = 'testpassword123';
/** Find a specific note card by its title text */
export function noteCard(page: Page, title: string): Locator {
return page.locator('[data-testid="note-card"]', { hasText: title });
}
/**
* Extended test fixture that handles setup/auth.
*/
@@ -40,6 +45,10 @@ export async function setupAndLogin(page: Page) {
await page.waitForURL('/');
}
// else: already on main page (session still valid)
// Wait for full hydration after auth redirect (window.location.href causes full reload).
// networkidle ensures JS bundles have loaded and executed.
await page.waitForLoadState('networkidle');
}
export { expect };
+6 -12
View File
@@ -1,12 +1,6 @@
import { test, expect } from './helpers/fixtures.js';
import { test, expect, noteCard } from './helpers/fixtures.js';
test.describe('Notes CRUD', () => {
test('Scenario: Fresh account shows an empty notes list', async ({ authenticatedPage: page }) => {
// Given the user has no notes
// Then the empty state message is displayed
await expect(page.getByText('No crumbs yet')).toBeVisible();
});
test('Scenario: New note appears in the notes list after creation', async ({ authenticatedPage: page }) => {
// When the user creates a note titled "My First Note" with content "Hello world!"
await page.getByTestId('new-note-btn').click();
@@ -17,8 +11,7 @@ test.describe('Notes CRUD', () => {
await page.getByTestId('close-editor-btn').click();
// Then the note is visible in the notes list
await expect(page.getByTestId('note-card')).toBeVisible();
await expect(page.getByText('My First Note')).toBeVisible();
await expect(noteCard(page, 'My First Note')).toBeVisible();
});
test('Scenario: Edited note title is reflected in the notes list', async ({ authenticatedPage: page }) => {
@@ -31,7 +24,7 @@ test.describe('Notes CRUD', () => {
await page.getByTestId('close-editor-btn').click();
// When the user changes the title to "Updated Title"
await page.getByTestId('note-card').click();
await noteCard(page, 'Original Title').click();
await page.getByTestId('note-title-input').clear();
await page.getByTestId('note-title-input').fill('Updated Title');
await page.getByTestId('close-editor-btn').click();
@@ -47,8 +40,9 @@ test.describe('Notes CRUD', () => {
await page.getByTestId('close-editor-btn').click();
// When the user trashes the note
await page.getByTestId('note-card').hover();
await page.getByTestId('trash-btn').click();
const card = noteCard(page, 'Delete Me');
await card.hover();
await card.getByTestId('trash-btn').click();
// Then the note is no longer visible
await expect(page.getByText('Delete Me')).not.toBeVisible();
+5 -4
View File
@@ -1,4 +1,4 @@
import { test, expect } from './helpers/fixtures.js';
import { test, expect, noteCard } from './helpers/fixtures.js';
test.describe('Offline Support', () => {
test('Scenario: Sync status is visible to the user', async ({ authenticatedPage: page }) => {
@@ -28,7 +28,7 @@ test.describe('Offline Support', () => {
await page.context().setOffline(true);
// And the user edits the note title to "Offline Edited"
await page.getByTestId('note-card').click();
await noteCard(page, 'Offline Edit').click();
await page.getByTestId('note-title-input').clear();
await page.getByTestId('note-title-input').fill('Offline Edited');
await page.getByTestId('close-editor-btn').click();
@@ -107,8 +107,9 @@ test.describe('Offline Support', () => {
await page.context().setOffline(true);
// And the user trashes the note
await page.getByTestId('note-card').hover();
await page.getByTestId('trash-btn').click();
const card = noteCard(page, 'Offline Trash');
await card.hover();
await card.getByTestId('trash-btn').click();
// Then the note is no longer visible
await expect(page.getByText('Offline Trash')).not.toBeVisible();
+9 -7
View File
@@ -1,4 +1,4 @@
import { test, expect } from './helpers/fixtures.js';
import { test, expect, noteCard } from './helpers/fixtures.js';
test.describe('Organization Features', () => {
test('Scenario: Pinned note appears under the Pinned section', async ({ authenticatedPage: page }) => {
@@ -8,8 +8,9 @@ test.describe('Organization Features', () => {
await page.getByTestId('close-editor-btn').click();
// When the user pins the note
await page.getByTestId('note-card').hover();
await page.getByTestId('pin-btn').click();
const pinCard = noteCard(page, 'Pin Me');
await pinCard.hover();
await pinCard.getByTestId('pin-btn').click();
// Then the "Pinned" section is visible
await expect(page.getByText('Pinned')).toBeVisible();
@@ -22,8 +23,9 @@ test.describe('Organization Features', () => {
await page.getByTestId('close-editor-btn').click();
// When the user archives the note
await page.getByTestId('note-card').hover();
await page.getByTestId('archive-btn').click();
const archiveCard = noteCard(page, 'Archive Me');
await archiveCard.hover();
await archiveCard.getByTestId('archive-btn').click();
// Then the note is no longer visible in the main view
await expect(page.getByText('Archive Me')).not.toBeVisible();
@@ -40,7 +42,7 @@ test.describe('Organization Features', () => {
await page.getByTestId('close-editor-btn').click();
// Then the note is saved and visible in the notes list
await expect(page.getByTestId('note-card')).toBeVisible();
await expect(noteCard(page, 'Colored Note')).toBeVisible();
});
test('Scenario: Filtering by tag shows only matching notes', async ({ authenticatedPage: page }) => {
@@ -57,7 +59,7 @@ test.describe('Organization Features', () => {
await page.getByTestId('close-editor-btn').click();
// When the user filters by the #important tag
const tagChip = page.getByTestId('tag-chip').filter({ hasText: '#important' });
const tagChip = page.getByTestId('tag-filter').getByTestId('tag-chip').filter({ hasText: '#important' });
if (await tagChip.isVisible()) {
await tagChip.click();
+2 -2
View File
@@ -1,4 +1,4 @@
import { test, expect } from './helpers/fixtures.js';
import { test, expect, noteCard } from './helpers/fixtures.js';
test.describe('Search', () => {
test('Scenario: Searching by title returns the matching note', async ({ authenticatedPage: page }) => {
@@ -42,6 +42,6 @@ test.describe('Search', () => {
await page.waitForTimeout(500);
// Then no notes are displayed
await expect(page.getByTestId('note-card')).not.toBeVisible();
await expect(page.getByTestId('note-card')).toHaveCount(0);
});
});