From cac6c14a23c3dcd377e3060eb6dfff6be0ba2657 Mon Sep 17 00:00:00 2001 From: kristianwind Date: Thu, 17 Sep 2026 18:47:23 +0200 Subject: [PATCH] =?UTF-8?q?fix(beskeder):=20klokkens=20panel=20bliver=20in?= =?UTF-8?q?den=20for=20sk=C3=A6rmen=20p=C3=A5=20en=20telefon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Panelet hang under klokken med `right: 0` og en fast bredde på 340 px. Klokken sidder ikke yderst — der er en temaknap, et mellemrum og en kantafstand til højre for den — så der er kun omkring 330 px tilbage, og resten blev skubbet ud over venstre kant og klippet af. Målt til -10 px ved en skærmbredde på 390; på en smallere telefon mere. Overskriften "Beskeder" klæbede til kanten uden luft, hvilket er det synlige tegn. På en telefon hører panelet nu til skærmen i stedet for til klokken: det spænder fra kant til kant med en margen i hver side, under toplinjen. Samme knæk ved 820 px som toplinjens egen menuknap, så de to skifter form samtidig. Det får også `overscroll-behavior: contain`, så det at rulle i listen ikke trækker siden bagved med. Toplinjens underkant måles frem for at blive regnet ud. Bredden KUNNE trækkes fra i CSS, men så ville den her fil kende sine naboers størrelse, og den næste knap i toplinjen ville lave fejlen om igen uden at sige noget. --- og fire ustabile prøver mere --- Fladen tegner hver ændring med det samme og sender kaldet bagefter. Alt, man kan se, er altså sandt, før serveren har hørt om det, og en prøve, der genindlæser eller klikker videre i det hul, måler den gamle tilstand. `en monospace-linje kan forlades` ventede 1200 ms før en genindlæsning. Datoprøven ventede 400 ms. Et gæt om, hvor hurtig maskinen er, holder indtil CI er travl — begge fejlede i en fuld kørsel her. `en gruppe er en side med sine projekter` er den mest lærerige: trækket virkede, og `toPass` blev grøn på den lokale tilstand, hvorefter klikket videre hentede en gruppe uden projekter i. Prøvens egen kommentar gætter på et upålideligt HTML5-træk; det var gemningen, der ikke var nået frem. Monospace-prøven havde OVENI en anden fejl, som først viste sig, da den første var lukket: den klikkede "Ny note" og skrev med det samme. `create()` venter på serverens svar, og imens står den forrige notes editor stadig på skærmen, så teksten gik ind i den forrige note og den nye blev tom. Ny hjælper `nyNote()` venter på et tomt felt. Omkring 14 andre steder i filen har samme mønster; de er ikke skrevet om, fordi de består, og en samlet omskrivning ville bytte kendte prøver for ukendte. Det hul findes også for et menneske: klikker man Ny note og skriver straks, lander de første anslag i den forrige note. Det er millisekunder lokalt og sekunder på en dårlig forbindelse. Samme familie som #86, men tiden FØR svaret frem for efter. Noteret som et selvstændigt punkt, ikke rettet her. `noteGemt()` fra #88 er nu `gemt(side, sti)` med noteGemt, opgaveGemt og projektGemt ovenpå. Der står 23 faste ventetider tilbage, men ingen af dem lige før en genindlæsning eller en navigation. Fuld chromium-suite kørt to gange: 90 bestået, 0 fejlet, begge gange. Co-Authored-By: Claude Opus 5 --- web/e2e/smoke.spec.js | 113 ++++++++++++++++---- web/src/lib/components/Notifications.svelte | 45 +++++++- 2 files changed, 138 insertions(+), 20 deletions(-) diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index dfd992a..79d2bc6 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -39,26 +39,60 @@ async function projectAction(page, name) { } /** - * Venter på, at en notes gemning faktisk er nået frem til serveren. + * Venter på, at en gemning faktisk er nået frem til serveren. * - * Ikke på teksten "Gemt" i foden: den er hvilestanden. Feltet siger "Gemmer" mens - * en gemning kører og "Gemt" ellers — altså også før den første overhovedet er - * begyndt. En ventetid på den er grøn med det samme og venter på ingenting, og det - * har gjort tre prøver i den her fil ustabile. Svaret på PATCH'en kan der ventes - * på, og det er det eneste, der beviser, at serveren har hørt om teksten. + * Hele fladen tegner en ændring med det samme og sender kaldet bagefter — en note + * står i listen, et projekt hopper ind under sin gruppe, en beskrivelse står på + * siden — så alt, hvad man kan se, er sandt, før serveren har hørt om det. En + * prøve, der genindlæser eller klikker videre i det hul, måler den gamle tilstand + * og fejler på noget, der ikke er i stykker. * + * Og teksten "Gemt" i notefoden duer ikke som værn: den er hvilestanden. Feltet + * siger "Gemmer" mens en gemning kører og "Gemt" ellers — altså også før den + * første overhovedet er begyndt. En ventetid på den er grøn med det samme og + * venter på ingenting. Det gjorde fem prøver i den her fil ustabile. + * + * En fast ventetid er den samme fejl med et tal foran: `waitForTimeout(1200)` er + * et gæt om, hvor hurtig maskinen er, og det gæt holder indtil CI er travl. + * + * Svaret på PATCH'en er det eneste, der beviser, at serveren har hørt om det. * Skal kaldes FØR den handling, der udløser gemningen — ellers er svaret måske * allerede kommet og gået. */ -function noteGemt(side) { +function gemt(side, sti) { return side.waitForResponse( - (r) => - r.request().method() === 'PATCH' && - /\/notes\/[^/]+$/.test(new URL(r.url()).pathname) && - r.ok() + (r) => r.request().method() === 'PATCH' && sti.test(new URL(r.url()).pathname) && r.ok() ); } +const noteGemt = (side) => gemt(side, /\/notes\/[^/]+$/); +const opgaveGemt = (side) => gemt(side, /\/tasks\/[^/]+$/); +const projektGemt = (side) => gemt(side, /\/projects\/[^/]+$/); + +/** + * Laver en ny note og giver feltet tilbage, når det er DEN nye, der skrives i. + * + * `create()` venter på serverens svar, før noten er oprettet, og imens står den + * forrige notes editor stadig på skærmen. Et klik på "Ny note" efterfulgt af + * tastetryk med det samme skriver derfor i den forrige note — teksten havner det + * forkerte sted, og den nye note bliver liggende tom. Efter en genindlæsning + * findes den titel, prøven leder efter, slet ikke. + * + * Det tomme felt er værnet. Det er samme form som resten af filen: alt kan ses, + * før serveren har hørt om det, så det, der kan ventes på, er tilstanden bagefter. + * + * Bemærk at hullet også findes for et menneske: klikker man Ny note og skriver + * straks, lander de første anslag i den forrige note. Det er hurtigt på en lokal + * maskine og sekunder på en dårlig forbindelse. Noteret som et selvstændigt punkt. + */ +async function nyNote(side) { + await side.getByRole('button', { name: 'Ny note' }).click(); + const felt = side.getByRole('textbox', { name: 'Notens tekst' }); + await expect(felt).toHaveText(''); + await felt.click(); + return felt; +} + function watchForTrouble(page) { const trouble = []; page.on('console', (message) => { @@ -1704,6 +1738,11 @@ test('en gruppe er en side med sine projekter, en beskrivelse og filer', async ( // HTML5 drag occasionally does not register the first time under CI load, and a // single dragTo then leaves the group empty and the page assertion failing for a // reason that has nothing to do with what the test is about. + // Sidebjælken viser flytningen med det samme og sender PATCH'en bagefter, så + // `nested` er sand, før serveren har hørt om den. Klikker prøven videre i det + // hul, henter gruppesiden en gruppe uden projekter i — og fejler på noget, der + // ikke er i stykker. Det er samme form som beskrivelsen længere nede. + const flyttet = projektGemt(page); const nested = sidebar.locator('.folder > a').filter({ hasText: 'Drejebænk' }); await expect(async () => { await sidebar @@ -1711,6 +1750,7 @@ test('en gruppe er en side med sine projekter, en beskrivelse og filer', async ( .dragTo(sidebar.getByRole('link', { name: 'Værksted' })); await expect(nested).toHaveCount(1, { timeout: 1000 }); }).toPass({ timeout: 10000 }); + await flyttet; await sidebar.getByRole('link', { name: 'Værksted' }).click(); await expect(page.getByRole('heading', { name: 'Værksted', level: 1 })).toBeVisible(); @@ -3739,9 +3779,7 @@ test('en monospace-linje kan forlades — på retur og gennem menuen', async ({ const trouble = watchForTrouble(page); await page.goto('/noter'); - await page.getByRole('button', { name: 'Ny note' }).click(); - const ed = page.getByRole('textbox', { name: 'Notens tekst' }); - await ed.click(); + const ed = await nyNote(page); await page.keyboard.type('Titel'); await page.keyboard.press('Enter'); @@ -3777,8 +3815,7 @@ test('en monospace-linje kan forlades — på retur og gennem menuen', async ({ // Det overlever turen gennem Markdown. En ren note for sig, så prøven ikke // afhænger af alt det ovenfor: kode, retur, brødtekst — gem, hent igen. - await page.getByRole('button', { name: 'Ny note' }).click(); - await ed.click(); + await nyNote(page); await page.keyboard.type('Rundtur'); await page.keyboard.press('Enter'); await page.getByRole('button', { name: 'Formatér' }).click(); @@ -3786,8 +3823,9 @@ test('en monospace-linje kan forlades — på retur og gennem menuen', async ({ await page.keyboard.type('docker pull'); await page.keyboard.press('Enter'); await page.keyboard.type('og så videre i almindelig skrift'); + const rundturGemt = noteGemt(page); await ed.blur(); - await page.waitForTimeout(1200); + await rundturGemt; await page.reload(); await page.getByRole('button', { name: /Rundtur/ }).click(); await expect(ed.locator('pre')).toContainText('docker pull'); @@ -4089,9 +4127,10 @@ test('en opgave kan få et klokkeslæt, og beholder det når den flyttes', async // Og datoen kan rettes bagefter uden at tage timen med sig. const date = page.locator('#due'); const day = await date.inputValue(); + const datoGemt = opgaveGemt(page); await date.fill(day); await date.blur(); - await page.waitForTimeout(400); + await datoGemt; await page.reload(); await page.getByText('ringe til tandlægen').click(); await expect( @@ -4846,6 +4885,44 @@ test('et udfald siger ingen forbindelse frem for at bede om kodeordet', async ({ await expect(page.getByLabel('Adgangskode')).toHaveCount(0); }); +/** + * Klokkens panel bliver inden for skærmen på en telefon. + * + * Panelet hang under klokken og var 340 px bredt. Klokken sidder tættere på højre + * kant end det — der er en temaknap og en kantafstand til højre for den — så + * panelet blev skubbet ud over venstre side og klippet af. På en telefon stod der + * flere linjer tekst uden for skærmen, og overskriften "Beskeder" lå klods op ad + * kanten uden luft, hvilket er tegnet på, at der mangler noget til venstre for + * den. + * + * Målt på kasserne frem for på et skærmbillede: "ser rigtigt ud" er ikke en + * påstand, en prøve kan efterprøve, men "venstre kant er ikke negativ" er. Begge + * kanter tjekkes — en rettelse, der skubber panelet ind fra venstre ved at lade + * det stikke ud til højre, er ikke en rettelse. + */ +test('klokkens panel bliver inden for skærmen på en telefon', async ({ page }) => { + const trouble = watchForTrouble(page); + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto('/'); + + await page.getByRole('button', { name: /^Beskeder/ }).click(); + const panel = page.getByRole('dialog', { name: 'Beskeder' }); + await expect(panel).toBeVisible(); + + const kasse = await panel.boundingBox(); + expect(kasse.x, 'panelet hænger ud over venstre kant').toBeGreaterThanOrEqual(0); + expect(kasse.x + kasse.width, 'panelet hænger ud over højre kant').toBeLessThanOrEqual(390); + + // Og der er luft til overskriften. Et panel, der lige akkurat begynder ved nul, + // men hvis tekst gør det samme, er stadig klippet — det var netop sådan, det så + // ud på telefonen. + const overskrift = panel.getByText('Beskeder', { exact: true }); + const tekst = await overskrift.boundingBox(); + expect(tekst.x, 'overskriften klæber til kanten').toBeGreaterThan(kasse.x); + + expect(trouble).toEqual([]); +}); + /** * Et indsat billede kan ses i fuld størrelse og kopieres videre. * diff --git a/web/src/lib/components/Notifications.svelte b/web/src/lib/components/Notifications.svelte index 3a7dfd2..03eb973 100644 --- a/web/src/lib/components/Notifications.svelte +++ b/web/src/lib/components/Notifications.svelte @@ -20,6 +20,31 @@ let open = $state(false); let wrap = $state(null); + /** + * Hvor panelet begynder på en telefon — målt, ikke regnet ud. + * + * På en bred skærm hænger panelet under klokken og hører til den. På en telefon + * kan det ikke: klokken sidder tættere på højre kant end panelet er bredt, så et + * panel, der er stillet op efter klokken, skydes ud over venstre side og bliver + * klippet. Der stod tre en halv linje tekst uden for skærmen. + * + * Bredden kunne trækkes fra i CSS — der er en temaknap, et mellemrum og en + * kantafstand til højre for klokken — men så ville den her fil kende sine + * naboers størrelse, og næste knap i toplinjen ville lave fejlen om igen uden + * at sige noget. Toplinjens underkant måles i stedet, og så spænder panelet over + * skærmen med en kant i hver side. + */ + let below = $state(0); + + function place() { + if (wrap) below = Math.round(wrap.getBoundingClientRect().bottom); + } + + function toggle() { + open = !open; + if (open) place(); + } + const KINDS = { assigned: 'notif.assigned', 'note.changed': 'notif.noteChanged', @@ -72,6 +97,7 @@ open && place()} onkeydown={(e) => { if (e.key === 'Escape' && open) open = false; }} @@ -81,7 +107,7 @@ {#if open} -