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:
181
keep-notes/tests/drag-drop.spec.ts
Normal file
181
keep-notes/tests/drag-drop.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
423
keep-notes/tests/reminder-dialog.spec.ts
Normal file
423
keep-notes/tests/reminder-dialog.spec.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Note Input - Reminder Dialog', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// Expand the note input
|
||||
await page.click('input[placeholder="Take a note..."]');
|
||||
await expect(page.locator('input[placeholder="Title"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open dialog when clicking Bell icon (not prompt)', async ({ page }) => {
|
||||
// Set up listener for prompt dialogs - should NOT appear
|
||||
let promptAppeared = false;
|
||||
page.on('dialog', () => {
|
||||
promptAppeared = true;
|
||||
});
|
||||
|
||||
// Click the Bell button
|
||||
const bellButton = page.locator('button:has(svg.lucide-bell)');
|
||||
await bellButton.click();
|
||||
|
||||
// Verify dialog opened (NOT a browser prompt)
|
||||
const dialog = page.locator('[role="dialog"]');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Verify dialog title
|
||||
await expect(page.locator('h2:has-text("Set Reminder")')).toBeVisible();
|
||||
|
||||
// Verify no prompt appeared
|
||||
expect(promptAppeared).toBe(false);
|
||||
|
||||
// Verify date and time inputs exist
|
||||
await expect(page.locator('input[type="date"]')).toBeVisible();
|
||||
await expect(page.locator('input[type="time"]')).toBeVisible();
|
||||
|
||||
// Verify buttons
|
||||
await expect(page.locator('button:has-text("Cancel")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Set Reminder")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have default values (tomorrow 9am)', async ({ page }) => {
|
||||
// Click Bell
|
||||
await page.click('button:has(svg.lucide-bell)');
|
||||
|
||||
// Get tomorrow's date
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const expectedDate = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
// Check date input
|
||||
const dateInput = page.locator('input[type="date"]');
|
||||
await expect(dateInput).toHaveValue(expectedDate);
|
||||
|
||||
// Check time input
|
||||
const timeInput = page.locator('input[type="time"]');
|
||||
await expect(timeInput).toHaveValue('09:00');
|
||||
});
|
||||
|
||||
test('should close dialog on Cancel', async ({ page }) => {
|
||||
// Open dialog
|
||||
await page.click('button:has(svg.lucide-bell)');
|
||||
await expect(page.locator('[role="dialog"]')).toBeVisible();
|
||||
|
||||
// Click Cancel
|
||||
await page.click('button:has-text("Cancel")');
|
||||
|
||||
// Dialog should close
|
||||
await expect(page.locator('[role="dialog"]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should close dialog on X button', async ({ page }) => {
|
||||
// Open dialog
|
||||
await page.click('button:has(svg.lucide-bell)');
|
||||
await expect(page.locator('[role="dialog"]')).toBeVisible();
|
||||
|
||||
// Click X button (close button in dialog)
|
||||
const closeButton = page.locator('[role="dialog"] button[data-slot="dialog-close"]');
|
||||
await closeButton.click();
|
||||
|
||||
// Dialog should close
|
||||
await expect(page.locator('[role="dialog"]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should validate empty date/time', async ({ page }) => {
|
||||
// Open dialog
|
||||
await page.click('button:has(svg.lucide-bell)');
|
||||
|
||||
// Clear date
|
||||
const dateInput = page.locator('input[type="date"]');
|
||||
await dateInput.fill('');
|
||||
|
||||
// Click Set Reminder
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
|
||||
// Should show warning toast (not close dialog)
|
||||
// We look for the toast notification
|
||||
await expect(page.locator('text=Please enter date and time')).toBeVisible();
|
||||
|
||||
// Dialog should still be open
|
||||
await expect(page.locator('[role="dialog"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should validate past date', async ({ page }) => {
|
||||
// Open dialog
|
||||
await page.click('button:has(svg.lucide-bell)');
|
||||
|
||||
// Set date to yesterday
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const pastDate = yesterday.toISOString().split('T')[0];
|
||||
|
||||
const dateInput = page.locator('input[type="date"]');
|
||||
await dateInput.fill(pastDate);
|
||||
|
||||
// Click Set Reminder
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
|
||||
// Should show error toast
|
||||
await expect(page.locator('text=Reminder must be in the future')).toBeVisible();
|
||||
|
||||
// Dialog should still be open
|
||||
await expect(page.locator('[role="dialog"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should set reminder successfully with valid date', async ({ page }) => {
|
||||
// Open dialog
|
||||
await page.click('button:has(svg.lucide-bell)');
|
||||
|
||||
// Set future date (tomorrow already default)
|
||||
const timeInput = page.locator('input[type="time"]');
|
||||
await timeInput.fill('14:30');
|
||||
|
||||
// Click Set Reminder
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
|
||||
// Should show success toast
|
||||
await expect(page.locator('text=/Reminder set for/')).toBeVisible();
|
||||
|
||||
// Dialog should close
|
||||
await expect(page.locator('[role="dialog"]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should clear fields after successful reminder', async ({ page }) => {
|
||||
// Open dialog
|
||||
await page.click('button:has(svg.lucide-bell)');
|
||||
|
||||
// Set and confirm
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
|
||||
// Wait for dialog to close
|
||||
await expect(page.locator('[role="dialog"]')).not.toBeVisible();
|
||||
|
||||
// Open again
|
||||
await page.click('button:has(svg.lucide-bell)');
|
||||
|
||||
// Should have default values again (tomorrow 9am)
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const expectedDate = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
await expect(page.locator('input[type="date"]')).toHaveValue(expectedDate);
|
||||
await expect(page.locator('input[type="time"]')).toHaveValue('09:00');
|
||||
});
|
||||
|
||||
test('should allow custom date and time selection', async ({ page }) => {
|
||||
// Open dialog
|
||||
await page.click('button:has(svg.lucide-bell)');
|
||||
|
||||
// Set custom date (next week)
|
||||
const nextWeek = new Date();
|
||||
nextWeek.setDate(nextWeek.getDate() + 7);
|
||||
const customDate = nextWeek.toISOString().split('T')[0];
|
||||
|
||||
const dateInput = page.locator('input[type="date"]');
|
||||
await dateInput.fill(customDate);
|
||||
|
||||
// Set custom time
|
||||
const timeInput = page.locator('input[type="time"]');
|
||||
await timeInput.fill('15:45');
|
||||
|
||||
// Submit
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
|
||||
// Should show success with the date/time
|
||||
await expect(page.locator('text=/Reminder set for/')).toBeVisible();
|
||||
await expect(page.locator('[role="dialog"]')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Note Editor - Reminder Dialog', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Create a test note
|
||||
await page.click('input[placeholder="Take a note..."]');
|
||||
await page.fill('input[placeholder="Title"]', 'Test Note for Reminder');
|
||||
await page.fill('textarea[placeholder="Take a note..."]', 'This note will have a reminder');
|
||||
await page.click('button:has-text("Add")');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Open the note for editing
|
||||
await page.click('text=Test Note for Reminder');
|
||||
await page.waitForTimeout(300);
|
||||
});
|
||||
|
||||
test('should open reminder dialog in note editor', async ({ page }) => {
|
||||
// Click the Bell button in note editor
|
||||
const bellButton = page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first();
|
||||
await bellButton.click();
|
||||
|
||||
// Should open a second dialog for reminder
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Verify reminder dialog opened
|
||||
await expect(page.locator('h2:has-text("Set Reminder")')).toBeVisible();
|
||||
|
||||
// Verify date and time inputs exist
|
||||
await expect(page.locator('input[type="date"]')).toBeVisible();
|
||||
await expect(page.locator('input[type="time"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should set reminder on existing note', async ({ page }) => {
|
||||
// Click Bell button
|
||||
await page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first().click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Set date and time
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const dateString = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
await page.fill('input[type="date"]', dateString);
|
||||
await page.fill('input[type="time"]', '14:30');
|
||||
|
||||
// Click Set Reminder
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
|
||||
// Should show success toast
|
||||
await expect(page.locator('text=/Reminder set for/')).toBeVisible();
|
||||
|
||||
// Reminder dialog should close
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.locator('h2:has-text("Set Reminder")')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should show bell button as active when reminder is set', async ({ page }) => {
|
||||
// Set reminder
|
||||
await page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first().click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const dateString = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
await page.fill('input[type="date"]', dateString);
|
||||
await page.fill('input[type="time"]', '10:00');
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Bell button should have active styling (text-blue-600)
|
||||
const bellButton = page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first();
|
||||
const className = await bellButton.getAttribute('class');
|
||||
expect(className).toContain('text-blue-600');
|
||||
});
|
||||
|
||||
test('should allow editing existing reminder', async ({ page }) => {
|
||||
// Set initial reminder
|
||||
await page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first().click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const dateString = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
await page.fill('input[type="date"]', dateString);
|
||||
await page.fill('input[type="time"]', '10:00');
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Open reminder dialog again
|
||||
await page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first().click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Should show previous values
|
||||
const dateInput = page.locator('input[type="date"]');
|
||||
const timeInput = page.locator('input[type="time"]');
|
||||
|
||||
await expect(dateInput).toHaveValue(dateString);
|
||||
await expect(timeInput).toHaveValue('10:00');
|
||||
|
||||
// Change time
|
||||
await timeInput.fill('15:00');
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
|
||||
await expect(page.locator('text=/Reminder set for/')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should allow removing reminder', async ({ page }) => {
|
||||
// Set reminder first
|
||||
await page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first().click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const dateString = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
await page.fill('input[type="date"]', dateString);
|
||||
await page.fill('input[type="time"]', '10:00');
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Open reminder dialog again
|
||||
await page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first().click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Should see "Remove Reminder" button
|
||||
await expect(page.locator('button:has-text("Remove Reminder")')).toBeVisible();
|
||||
|
||||
// Click Remove Reminder
|
||||
await page.click('button:has-text("Remove Reminder")');
|
||||
|
||||
// Should show success toast
|
||||
await expect(page.locator('text=Reminder removed')).toBeVisible();
|
||||
|
||||
// Bell button should not be active anymore
|
||||
await page.waitForTimeout(300);
|
||||
const bellButton = page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first();
|
||||
const className = await bellButton.getAttribute('class');
|
||||
expect(className).not.toContain('text-blue-600');
|
||||
});
|
||||
|
||||
test('should persist reminder after saving note', async ({ page }) => {
|
||||
// Set reminder
|
||||
await page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first().click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const dateString = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
await page.fill('input[type="date"]', dateString);
|
||||
await page.fill('input[type="time"]', '14:00');
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Save the note
|
||||
await page.click('button:has-text("Save")');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Reopen the note
|
||||
await page.click('text=Test Note for Reminder');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Bell button should still be active
|
||||
const bellButton = page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first();
|
||||
const className = await bellButton.getAttribute('class');
|
||||
expect(className).toContain('text-blue-600');
|
||||
|
||||
// Open reminder dialog to verify values
|
||||
await bellButton.click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const dateInput = page.locator('input[type="date"]');
|
||||
const timeInput = page.locator('input[type="time"]');
|
||||
|
||||
await expect(dateInput).toHaveValue(dateString);
|
||||
await expect(timeInput).toHaveValue('14:00');
|
||||
});
|
||||
|
||||
test('should show bell icon on note card when reminder is set', async ({ page }) => {
|
||||
// Set reminder
|
||||
await page.locator('[role="dialog"]:visible button:has(svg.lucide-bell)').first().click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const dateString = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
await page.fill('input[type="date"]', dateString);
|
||||
await page.fill('input[type="time"]', '10:00');
|
||||
await page.click('button:has-text("Set Reminder")');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Save and close
|
||||
await page.click('button:has-text("Save")');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check that note card has bell icon
|
||||
const noteCard = page.locator('text=Test Note for Reminder').locator('..');
|
||||
await expect(noteCard.locator('svg.lucide-bell')).toBeVisible();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
// Close any open dialogs
|
||||
const dialogs = page.locator('[role="dialog"]');
|
||||
const count = await dialogs.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const cancelButton = page.locator('button:has-text("Cancel")').first();
|
||||
if (await cancelButton.isVisible()) {
|
||||
await cancelButton.click();
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete test note
|
||||
try {
|
||||
const testNote = page.locator('text=Test Note for Reminder').first();
|
||||
if (await testNote.isVisible()) {
|
||||
await testNote.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);
|
||||
}
|
||||
} catch (e) {
|
||||
// Note might already be deleted
|
||||
}
|
||||
});
|
||||
});
|
||||
167
keep-notes/tests/undo-redo.spec.ts
Normal file
167
keep-notes/tests/undo-redo.spec.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Note Input - Undo/Redo', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// Expand the note input
|
||||
await page.click('input[placeholder="Take a note..."]');
|
||||
await expect(page.locator('input[placeholder="Title"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should save history after 1 second of inactivity', async ({ page }) => {
|
||||
const contentArea = page.locator('textarea[placeholder="Take a note..."]');
|
||||
|
||||
// Type "Hello"
|
||||
await contentArea.fill('Hello');
|
||||
|
||||
// Wait for debounce to save
|
||||
await page.waitForTimeout(1100);
|
||||
|
||||
// Type " World"
|
||||
await contentArea.fill('Hello World');
|
||||
|
||||
// Wait for debounce
|
||||
await page.waitForTimeout(1100);
|
||||
|
||||
// Click Undo button
|
||||
const undoButton = page.locator('button:has(svg.lucide-undo-2)');
|
||||
await expect(undoButton).toBeEnabled();
|
||||
await undoButton.click();
|
||||
|
||||
// Should show "Hello" only
|
||||
await expect(contentArea).toHaveValue('Hello');
|
||||
|
||||
// Undo should now be disabled (back to initial state)
|
||||
// Actually not disabled, there's the initial empty state
|
||||
await undoButton.click();
|
||||
await expect(contentArea).toHaveValue('');
|
||||
|
||||
// Undo should be disabled now
|
||||
await expect(undoButton).toBeDisabled();
|
||||
|
||||
// Click Redo
|
||||
const redoButton = page.locator('button:has(svg.lucide-redo-2)');
|
||||
await expect(redoButton).toBeEnabled();
|
||||
await redoButton.click();
|
||||
|
||||
// Should show "Hello"
|
||||
await expect(contentArea).toHaveValue('Hello');
|
||||
|
||||
// Redo again
|
||||
await redoButton.click();
|
||||
await expect(contentArea).toHaveValue('Hello World');
|
||||
|
||||
// Redo should be disabled now
|
||||
await expect(redoButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should undo/redo with keyboard shortcuts', async ({ page }) => {
|
||||
const contentArea = page.locator('textarea[placeholder="Take a note..."]');
|
||||
|
||||
// Type and wait
|
||||
await contentArea.fill('First');
|
||||
await page.waitForTimeout(1100);
|
||||
|
||||
await contentArea.fill('Second');
|
||||
await page.waitForTimeout(1100);
|
||||
|
||||
// Ctrl+Z to undo
|
||||
await page.keyboard.press('Control+z');
|
||||
await expect(contentArea).toHaveValue('First');
|
||||
|
||||
// Ctrl+Z again
|
||||
await page.keyboard.press('Control+z');
|
||||
await expect(contentArea).toHaveValue('');
|
||||
|
||||
// Ctrl+Y to redo
|
||||
await page.keyboard.press('Control+y');
|
||||
await expect(contentArea).toHaveValue('First');
|
||||
|
||||
// Ctrl+Shift+Z also works for redo
|
||||
await page.keyboard.press('Control+Shift+z');
|
||||
await expect(contentArea).toHaveValue('Second');
|
||||
});
|
||||
|
||||
test('should work with title and content', async ({ page }) => {
|
||||
const titleInput = page.locator('input[placeholder="Title"]');
|
||||
const contentArea = page.locator('textarea[placeholder="Take a note..."]');
|
||||
|
||||
// Type title
|
||||
await titleInput.fill('My Title');
|
||||
await page.waitForTimeout(1100);
|
||||
|
||||
// Type content
|
||||
await contentArea.fill('My Content');
|
||||
await page.waitForTimeout(1100);
|
||||
|
||||
// Undo
|
||||
await page.keyboard.press('Control+z');
|
||||
await expect(titleInput).toHaveValue('My Title');
|
||||
await expect(contentArea).toHaveValue('');
|
||||
|
||||
// Undo again
|
||||
await page.keyboard.press('Control+z');
|
||||
await expect(titleInput).toHaveValue('');
|
||||
await expect(contentArea).toHaveValue('');
|
||||
});
|
||||
|
||||
test('should reset history after creating note', async ({ page }) => {
|
||||
const contentArea = page.locator('textarea[placeholder="Take a note..."]');
|
||||
const undoButton = page.locator('button:has(svg.lucide-undo-2)');
|
||||
|
||||
// Type something
|
||||
await contentArea.fill('Test note');
|
||||
await page.waitForTimeout(1100);
|
||||
|
||||
// Undo should be enabled
|
||||
await expect(undoButton).toBeEnabled();
|
||||
|
||||
// Submit note
|
||||
await page.click('button:has-text("Add")');
|
||||
|
||||
// Wait for note to be created and form to reset
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Expand again
|
||||
await page.click('input[placeholder="Take a note..."]');
|
||||
|
||||
// Undo should be disabled (fresh start)
|
||||
await expect(undoButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should not create history during undo/redo', async ({ page }) => {
|
||||
const contentArea = page.locator('textarea[placeholder="Take a note..."]');
|
||||
|
||||
// Type "A"
|
||||
await contentArea.fill('A');
|
||||
await page.waitForTimeout(1100);
|
||||
|
||||
// Type "B"
|
||||
await contentArea.fill('B');
|
||||
await page.waitForTimeout(1100);
|
||||
|
||||
// Type "C"
|
||||
await contentArea.fill('C');
|
||||
await page.waitForTimeout(1100);
|
||||
|
||||
// Undo to B
|
||||
await page.keyboard.press('Control+z');
|
||||
await expect(contentArea).toHaveValue('B');
|
||||
|
||||
// Undo to A
|
||||
await page.keyboard.press('Control+z');
|
||||
await expect(contentArea).toHaveValue('A');
|
||||
|
||||
// Redo to B
|
||||
await page.keyboard.press('Control+y');
|
||||
await expect(contentArea).toHaveValue('B');
|
||||
|
||||
// Redo to C
|
||||
await page.keyboard.press('Control+y');
|
||||
await expect(contentArea).toHaveValue('C');
|
||||
|
||||
// Should not be able to redo further
|
||||
const redoButton = page.locator('button:has(svg.lucide-redo-2)');
|
||||
await expect(redoButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user