diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index 850ad4b..3ef953b 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -3038,3 +3038,57 @@ test('flere noter kan markeres, arkiveres og hentes frem igen', async ({ page }) expect(trouble).toEqual([]); }); + +/** + * Én note, der ikke kan tegnes, må ikke tage ruden med sig. + * + * Meldt ind som "jeg kan ikke skifte mellem noter — uanset hvilken jeg trykker + * på, åbner de ikke". Editoren indlæste med `note.body.trim()` uden værn, og + * `body` er en streng fra serveren hver eneste gang — indtil den ikke er. Så + * kaster den inde i et `$effect`, og en effekt, der kaster, tager komponentens + * opdateringer med sig: den forrige note bliver stående, og ingen note kan åbnes + * bagefter. Klikket kan ikke engang lande. + * + * Formen fremtvinges her frem for at vente på den nyttelast, der gjorde det: + * hvad der end fik `body` til at mangle, er svaret det samme — én note må ikke + * kunne låse de tolv hundrede andre ude. + */ +test('en note uden krop må ikke låse ruden', async ({ page }) => { + const errors = []; + page.on('pageerror', (e) => errors.push(e.message)); + + await page.goto('/noter'); + for (const title of ['Alfa', 'Beta']) { + await page.getByRole('button', { name: 'Ny note' }).click(); + const ed = page.getByRole('textbox', { name: 'Notens tekst' }); + await ed.click(); + await page.keyboard.type(title); + await expect(ed.locator('h1')).toHaveText(title); + await page.keyboard.press('Enter'); + await page.keyboard.type('Brødtekst i ' + title); + await expect(ed.locator('p').filter({ hasText: 'Brødtekst i ' + title })).toBeVisible(); + await page.waitForTimeout(900); + } + await page.reload(); + await expect(page.locator('li .row strong').filter({ hasText: 'Beta' })).toBeVisible(); + + await page.route('**/api/v1/notes/*', async (route) => { + if (route.request().method() !== 'GET') return route.continue(); + const res = await route.fetch(); + const note = await res.json(); + delete note.body; + await route.fulfill({ response: res, json: note }); + }); + + await page.locator('li').filter({ hasText: 'Alfa' }).first().click(); + await page.waitForTimeout(600); + expect(errors, 'noten kastede under indlæsningen').toEqual([]); + + await page.unroute('**/api/v1/notes/*'); + await page.locator('li').filter({ hasText: 'Beta' }).first().click(); + await expect( + page.getByRole('textbox', { name: 'Notens tekst' }).locator('h1'), + 'ruden er låst: den næste note åbner ikke' + ).toHaveText('Beta', { timeout: 5000 }); + expect(errors, 'ruden efterlod en fejl bag sig').toEqual([]); +}); diff --git a/web/src/lib/components/NoteEditor.svelte b/web/src/lib/components/NoteEditor.svelte index 782e85a..f88831f 100644 --- a/web/src/lib/components/NoteEditor.svelte +++ b/web/src/lib/components/NoteEditor.svelte @@ -35,20 +35,53 @@ let loadedId = $state(null); $effect(() => { if (!editor || !note || note.id === loadedId) return; - // An empty note opens on its title. That is what Apple Notes does, and it is - // also what the list needs: the first line becomes the name of the note, so a - // note whose first line is body text is a note called by its first sentence. + + // Sat *før* tegningen, ikke efter. // - // Tested on the text and not on the rendered result: an empty body renders as - // one empty paragraph, not as nothing, so a falsy check here never fired. - editor.innerHTML = note.body.trim() ? markdownToHtml(note.body) : '


'; + // Kaster noget herunder, er noten stadig den, der er forsøgt indlæst — og + // uden det her ville effekten prøve igen ved hver eneste ændring bagefter og + // kaste hver gang. loadedId = note.id; active = {}; // Kildevisningen hører til den note, den blev slået til på. Skifter man note, // er det den rige flade, man skal møde — ellers står den næste note som kode, // uden at nogen har bedt om det. asSource = false; - colourCode(); + + // Én note, der ikke kan tegnes, må ikke tage ruden med sig. + // + // Det var `note.body.trim()` uden værn, og `body` er en streng fra serveren + // hver eneste gang — indtil den ikke er. Så kaster den her inde i et + // `$effect`, og en effekt, der kaster, tager komponentens opdateringer med + // sig: den forrige note bliver stående, og *ingen* note kan åbnes bagefter. + // Klikket kan ikke engang lande. Meldt ind som "jeg kan ikke skifte mellem + // noter, uanset hvilken jeg trykker på", hvilket er præcis, hvad det ser ud + // som fra den anden side. + // + // Fejlen står i konsollen frem for i en toast: den hører til den, der kan + // rette den, og en note, der viser sin egen tekst uden formatering, siger + // allerede til den, der læser den, at noget er galt. + // + // `textContent` og ikke `innerHTML` i faldbakken — teksten er ikke tegnet, og + // at lægge den ind som markup ville være at køre det, tegningen lige nægtede. + // Gemningen er ikke i fare: den skriver `draft`, som kommer fra svaret og + // ikke fra det, der står i editoren, indtil nogen taster i den. + const body = note.body ?? ''; + try { + // An empty note opens on its title. That is what Apple Notes does, and it + // is also what the list needs: the first line becomes the name of the + // note, so a note whose first line is body text is a note called by its + // first sentence. + // + // Tested on the text and not on the rendered result: an empty body renders + // as one empty paragraph, not as nothing, so a falsy check here never + // fired. + editor.innerHTML = body.trim() ? markdownToHtml(body) : '


'; + colourCode(); + } catch (e) { + editor.textContent = body; + console.error('verdande: noten kunne ikke tegnes', note.id, e); + } }); const SYNTAX = [