fix(editor): make sure checkbox lists are unique (#2007)

This fixes a checkbox persistence bug where toggling one checkbox would affect others with identical text. To make this work, a new unique `data-task-id` attribute was added to each task list item for reliable identification.

Fixes #293, #563

🐰 With nanoid's magic and IDs so fine,
Each checkbox now knows which line is mine,
No more lost state when the page reloads—
Tasks persist through every browser node!
This commit is contained in:
kolaente
2025-12-19 17:12:31 +01:00
committed by GitHub
parent 84791aa996
commit ef1efbc29a
6 changed files with 308 additions and 12 deletions

View File

@@ -969,6 +969,60 @@ test.describe('Task', () => {
await expect(page.locator('.tiptap__editor ul > li input[type=checkbox]').first()).toBeChecked()
})
test('Loads old checklist data without task IDs and generates unique IDs (backwards compatibility)', async ({authenticatedPage: page}) => {
// Old checklist HTML format without data-task-id attributes
// This simulates data saved before the checkbox persistence fix
const tasks = await TaskFactory.create(1, {
id: 1,
description: `
<ul data-type="taskList">
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Item One</p></div>
</li>
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Item Two</p></div>
</li>
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Item Three</p></div>
</li>
</ul>`,
})
await page.goto(`/tasks/${tasks[0].id}`)
// Verify all checkboxes are rendered
await expect(page.locator('.tiptap__editor ul > li input[type=checkbox]')).toHaveCount(3)
await expect(page.locator('.task-view .checklist-summary')).toContainText('0 of 3 tasks')
// Check that unique IDs were generated for each task item
const taskIds = await page.evaluate(() => {
const items = document.querySelectorAll('.tiptap__editor [data-task-id]')
return Array.from(items).map(el => el.getAttribute('data-task-id'))
})
expect(taskIds).toHaveLength(3)
// All IDs should be unique
const uniqueIds = new Set(taskIds)
expect(uniqueIds.size).toBe(3)
// Toggle only the second checkbox
await page.locator('.tiptap__editor ul > li input[type=checkbox]').nth(1).click()
await expect(page.locator('.task-view .details.content.description h3 span.is-small.has-text-success')).toContainText('Saved!')
await expect(page.locator('.task-view .checklist-summary')).toContainText('1 of 3 tasks')
// Verify only the second checkbox is checked, not the others
await expect(page.locator('.tiptap__editor ul > li input[type=checkbox]').nth(0)).not.toBeChecked()
await expect(page.locator('.tiptap__editor ul > li input[type=checkbox]').nth(1)).toBeChecked()
await expect(page.locator('.tiptap__editor ul > li input[type=checkbox]').nth(2)).not.toBeChecked()
// Reload and verify persistence
await page.reload()
await expect(page.locator('.task-view .checklist-summary')).toContainText('1 of 3 tasks')
await expect(page.locator('.tiptap__editor ul > li input[type=checkbox]').nth(0)).not.toBeChecked()
await expect(page.locator('.tiptap__editor ul > li input[type=checkbox]').nth(1)).toBeChecked()
await expect(page.locator('.tiptap__editor ul > li input[type=checkbox]').nth(2)).not.toBeChecked()
})
test('Should use the editor to render description', async ({authenticatedPage: page}) => {
const tasks = await TaskFactory.create(1, {
id: 1,