fix(beskeder): klokkens panel bliver inden for skærmen på en telefon
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f632fb8643
commit
cac6c14a23
+95
-18
@@ -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
|
* Hele fladen tegner en ændring med det samme og sender kaldet bagefter — en note
|
||||||
* en gemning kører og "Gemt" ellers — altså også før den første overhovedet er
|
* står i listen, et projekt hopper ind under sin gruppe, en beskrivelse står på
|
||||||
* begyndt. En ventetid på den er grøn med det samme og venter på ingenting, og det
|
* siden — så alt, hvad man kan se, er sandt, før serveren har hørt om det. En
|
||||||
* har gjort tre prøver i den her fil ustabile. Svaret på PATCH'en kan der ventes
|
* prøve, der genindlæser eller klikker videre i det hul, måler den gamle tilstand
|
||||||
* på, og det er det eneste, der beviser, at serveren har hørt om teksten.
|
* 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
|
* Skal kaldes FØR den handling, der udløser gemningen — ellers er svaret måske
|
||||||
* allerede kommet og gået.
|
* allerede kommet og gået.
|
||||||
*/
|
*/
|
||||||
function noteGemt(side) {
|
function gemt(side, sti) {
|
||||||
return side.waitForResponse(
|
return side.waitForResponse(
|
||||||
(r) =>
|
(r) => r.request().method() === 'PATCH' && sti.test(new URL(r.url()).pathname) && r.ok()
|
||||||
r.request().method() === 'PATCH' &&
|
|
||||||
/\/notes\/[^/]+$/.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) {
|
function watchForTrouble(page) {
|
||||||
const trouble = [];
|
const trouble = [];
|
||||||
page.on('console', (message) => {
|
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
|
// 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
|
// 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.
|
// 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' });
|
const nested = sidebar.locator('.folder > a').filter({ hasText: 'Drejebænk' });
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await sidebar
|
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' }));
|
.dragTo(sidebar.getByRole('link', { name: 'Værksted' }));
|
||||||
await expect(nested).toHaveCount(1, { timeout: 1000 });
|
await expect(nested).toHaveCount(1, { timeout: 1000 });
|
||||||
}).toPass({ timeout: 10000 });
|
}).toPass({ timeout: 10000 });
|
||||||
|
await flyttet;
|
||||||
await sidebar.getByRole('link', { name: 'Værksted' }).click();
|
await sidebar.getByRole('link', { name: 'Værksted' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Værksted', level: 1 })).toBeVisible();
|
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);
|
const trouble = watchForTrouble(page);
|
||||||
await page.goto('/noter');
|
await page.goto('/noter');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Ny note' }).click();
|
const ed = await nyNote(page);
|
||||||
const ed = page.getByRole('textbox', { name: 'Notens tekst' });
|
|
||||||
await ed.click();
|
|
||||||
await page.keyboard.type('Titel');
|
await page.keyboard.type('Titel');
|
||||||
await page.keyboard.press('Enter');
|
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
|
// 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.
|
// afhænger af alt det ovenfor: kode, retur, brødtekst — gem, hent igen.
|
||||||
await page.getByRole('button', { name: 'Ny note' }).click();
|
await nyNote(page);
|
||||||
await ed.click();
|
|
||||||
await page.keyboard.type('Rundtur');
|
await page.keyboard.type('Rundtur');
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
await page.getByRole('button', { name: 'Formatér' }).click();
|
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.type('docker pull');
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
await page.keyboard.type('og så videre i almindelig skrift');
|
await page.keyboard.type('og så videre i almindelig skrift');
|
||||||
|
const rundturGemt = noteGemt(page);
|
||||||
await ed.blur();
|
await ed.blur();
|
||||||
await page.waitForTimeout(1200);
|
await rundturGemt;
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.getByRole('button', { name: /Rundtur/ }).click();
|
await page.getByRole('button', { name: /Rundtur/ }).click();
|
||||||
await expect(ed.locator('pre')).toContainText('docker pull');
|
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.
|
// Og datoen kan rettes bagefter uden at tage timen med sig.
|
||||||
const date = page.locator('#due');
|
const date = page.locator('#due');
|
||||||
const day = await date.inputValue();
|
const day = await date.inputValue();
|
||||||
|
const datoGemt = opgaveGemt(page);
|
||||||
await date.fill(day);
|
await date.fill(day);
|
||||||
await date.blur();
|
await date.blur();
|
||||||
await page.waitForTimeout(400);
|
await datoGemt;
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.getByText('ringe til tandlægen').click();
|
await page.getByText('ringe til tandlægen').click();
|
||||||
await expect(
|
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);
|
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.
|
* Et indsat billede kan ses i fuld størrelse og kopieres videre.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -20,6 +20,31 @@
|
|||||||
let open = $state(false);
|
let open = $state(false);
|
||||||
let wrap = $state(null);
|
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 = {
|
const KINDS = {
|
||||||
assigned: 'notif.assigned',
|
assigned: 'notif.assigned',
|
||||||
'note.changed': 'notif.noteChanged',
|
'note.changed': 'notif.noteChanged',
|
||||||
@@ -72,6 +97,7 @@
|
|||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
onclick={outside}
|
onclick={outside}
|
||||||
|
onresize={() => open && place()}
|
||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (e.key === 'Escape' && open) open = false;
|
if (e.key === 'Escape' && open) open = false;
|
||||||
}}
|
}}
|
||||||
@@ -81,7 +107,7 @@
|
|||||||
<button
|
<button
|
||||||
class="bell"
|
class="bell"
|
||||||
class:on={open}
|
class:on={open}
|
||||||
onclick={() => (open = !open)}
|
onclick={toggle}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
aria-label={app.unread
|
aria-label={app.unread
|
||||||
@@ -101,7 +127,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<div class="panel" role="dialog" aria-label={t('notif.title')}>
|
<div class="panel" role="dialog" aria-label={t('notif.title')} style="--below: {below}px">
|
||||||
<header>
|
<header>
|
||||||
<span>{t('notif.title')}</span>
|
<span>{t('notif.title')}</span>
|
||||||
{#if app.unread}
|
{#if app.unread}
|
||||||
@@ -193,6 +219,21 @@
|
|||||||
box-shadow: var(--shadow-lg, 0 8px 30px rgb(0 0 0 / 0.25));
|
box-shadow: var(--shadow-lg, 0 8px 30px rgb(0 0 0 / 0.25));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* På en telefon hører panelet til skærmen, ikke til klokken. Se `below` i
|
||||||
|
skriptet for hvorfor det måles frem for at blive regnet ud. Samme knæk som
|
||||||
|
toplinjens egen menuknap, så de to skifter form samtidig. */
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.panel {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(var(--below) + 6px);
|
||||||
|
left: var(--s3);
|
||||||
|
right: var(--s3);
|
||||||
|
width: auto;
|
||||||
|
max-height: calc(100dvh - var(--below) - var(--s3) * 2);
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.panel header {
|
.panel header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user