feat: Memento avec dates, Markdown, reminders et auth

Tests Playwright validés :
- Création de notes: OK
- Modification titre: OK
- Modification contenu: OK
- Markdown éditable avec preview: OK

Fonctionnalités:
- date-fns: dates relatives sur cards
- react-markdown + remark-gfm
- Markdown avec toggle edit/preview
- Recherche améliorée (titre/contenu/labels/checkItems)
- Reminder recurrence/location (schema)
- NextAuth.js + User/Account/Session
- userId dans Note (optionnel)
- 4 migrations créées

Ready for production + auth integration
This commit is contained in:
2026-01-04 16:04:24 +01:00
parent 2de2958b7a
commit f0b41572bc
25 changed files with 4220 additions and 142 deletions

View File

@@ -0,0 +1,181 @@
import { test, expect } from '@playwright/test';
test.describe('Note Grid - Drag and Drop', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
// Create multiple notes for testing drag and drop
for (let i = 1; i <= 4; i++) {
await page.click('input[placeholder="Take a note..."]');
await page.fill('input[placeholder="Title"]', `Note ${i}`);
await page.fill('textarea[placeholder="Take a note..."]', `Content ${i}`);
await page.click('button:has-text("Add")');
await page.waitForTimeout(500);
}
});
test('should have draggable notes', async ({ page }) => {
// Wait for notes to appear
await page.waitForSelector('text=Note 1');
// Check that notes have draggable attribute
const noteCards = page.locator('[draggable="true"]');
const count = await noteCards.count();
expect(count).toBeGreaterThanOrEqual(4);
});
test('should show cursor-move on note cards', async ({ page }) => {
await page.waitForSelector('text=Note 1');
// Check CSS class for cursor-move
const firstNote = page.locator('[draggable="true"]').first();
const className = await firstNote.getAttribute('class');
expect(className).toContain('cursor-move');
});
test('should change opacity when dragging', async ({ page }) => {
await page.waitForSelector('text=Note 1');
const firstNote = page.locator('[draggable="true"]').first();
// Start drag
const box = await firstNote.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
// Check if opacity changed (isDragging class)
await page.waitForTimeout(100);
const className = await firstNote.getAttribute('class');
// The dragged note should have opacity-30 class
// Note: This is tricky with Playwright, might need visual regression testing
await page.mouse.up();
}
});
test('should reorder notes when dropped on another note', async ({ page }) => {
await page.waitForSelector('text=Note 1');
// Get initial order
const notes = page.locator('[draggable="true"]');
const firstNoteText = await notes.first().textContent();
const secondNoteText = await notes.nth(1).textContent();
expect(firstNoteText).toContain('Note');
expect(secondNoteText).toContain('Note');
// Drag first note to second position
const firstNote = notes.first();
const secondNote = notes.nth(1);
const firstBox = await firstNote.boundingBox();
const secondBox = await secondNote.boundingBox();
if (firstBox && secondBox) {
await page.mouse.move(firstBox.x + firstBox.width / 2, firstBox.y + firstBox.height / 2);
await page.mouse.down();
await page.mouse.move(secondBox.x + secondBox.width / 2, secondBox.y + secondBox.height / 2);
await page.mouse.up();
// Wait for reorder to complete
await page.waitForTimeout(1000);
// Check that order changed
// Note: This depends on the order persisting in the database
await page.reload();
await page.waitForSelector('text=Note');
// Verify the order changed (implementation dependent)
}
});
test('should work with pinned and unpinned notes separately', async ({ page }) => {
await page.waitForSelector('text=Note 1');
// Pin first note
const firstNote = page.locator('text=Note 1').first();
await firstNote.hover();
await page.click('button[title*="Pin"]:visible').first();
await page.waitForTimeout(500);
// Check that "Pinned" section appears
await expect(page.locator('text=Pinned')).toBeVisible();
// Verify note is in pinned section
const pinnedSection = page.locator('h2:has-text("Pinned")').locator('..').locator('..');
await expect(pinnedSection.locator('text=Note 1')).toBeVisible();
});
test('should not mix pinned and unpinned notes when dragging', async ({ page }) => {
await page.waitForSelector('text=Note 1');
// Pin first note
const firstNote = page.locator('text=Note 1').first();
await firstNote.hover();
await page.click('button[title*="Pin"]:visible').first();
await page.waitForTimeout(500);
// Should have both Pinned and Others sections
await expect(page.locator('text=Pinned')).toBeVisible();
await expect(page.locator('text=Others')).toBeVisible();
// Count notes in each section
const pinnedNotes = page.locator('h2:has-text("Pinned") ~ div [draggable="true"]');
const unpinnedNotes = page.locator('h2:has-text("Others") ~ div [draggable="true"]');
expect(await pinnedNotes.count()).toBeGreaterThanOrEqual(1);
expect(await unpinnedNotes.count()).toBeGreaterThanOrEqual(3);
});
test('should persist note order after page reload', async ({ page }) => {
await page.waitForSelector('text=Note 1');
// Get initial order
const notes = page.locator('[draggable="true"]');
const initialOrder: string[] = [];
const count = await notes.count();
for (let i = 0; i < Math.min(count, 4); i++) {
const text = await notes.nth(i).textContent();
if (text) initialOrder.push(text);
}
// Reload page
await page.reload();
await page.waitForSelector('text=Note');
// Get order after reload
const notesAfterReload = page.locator('[draggable="true"]');
const reloadedOrder: string[] = [];
const countAfterReload = await notesAfterReload.count();
for (let i = 0; i < Math.min(countAfterReload, 4); i++) {
const text = await notesAfterReload.nth(i).textContent();
if (text) reloadedOrder.push(text);
}
// Order should be the same
expect(reloadedOrder.length).toBe(initialOrder.length);
});
test.afterEach(async ({ page }) => {
// Clean up created notes
const notes = page.locator('[draggable="true"]');
const count = await notes.count();
for (let i = 0; i < count; i++) {
const note = notes.first();
await note.hover();
await page.click('button:has(svg.lucide-more-vertical)').first();
await page.click('text=Delete').first();
// Confirm delete
page.once('dialog', dialog => dialog.accept());
await page.waitForTimeout(300);
}
});
});