fix(integrationer): en adresse kan læses helt, også på en telefon

Feed'et, mailadressen, krogen, curl-eksemplet, CalDAV-serveren og de to
redirect-URI'er lå i skrivebeskyttede <input>. Et input ombryder ikke — det
ruller indeni — så på en telefon stoppede adressen midt i kassen, og resten
fandtes kun ved at trække inde i feltet. Der var ikke engang et rullepanel at
se: feltet så ud som om det indeholdt det, der stod i det. Målt ved 390 px var
kassen 306 px og krogens adresse 634.

De er nu <output>. Det er stadig et felt, en <label for> kan pege på, så
navnene på felterne bliver ved med at høre sammen med dem, og det er tekst,
der ombryder. `user-select: all` beholder det ene, inputtet gjorde bedre: ét
tryk tager hele adressen.

Det samme gjaldt invitationslinket i en delt note, som vises netop når der
ikke er nogen post at sende det med — altså når det er det eneste, der findes.

Prøven nævner felterne ved navn frem for at forespørge på en klasse. Den, der
måler `.field output` og finder nul, består også den dag felterne er lavet om
til noget andet klippet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-09-12 09:18:49 +02:00
co-authored by Claude Opus 5
parent 72637a153a
commit 61f8887319
4 changed files with 80 additions and 20 deletions
+38 -7
View File
@@ -4362,19 +4362,25 @@ test('dagens plan står på I dag, og ikke kun som en besked', async ({ page })
});
/**
* En kalenderadresse gør ikke siden bredere end telefonen.
* Adresserne på integrationssiden kan ses helt på en telefon.
*
* Adressen på et abonnement er ét ord på et par hundrede tegn uden et mellemrum i,
* og den stod i en liste uden noget, der lod den ombryde. Det, man så, var ikke en
* linje, der løb ud over kanten — det var hele siden, der fik et vandret
* rullepanel, fordi det bredeste element på en side bestemmer sidens bredde. Derfor
* måler prøven dokumentet og ikke elementet: det er dokumentet, der var galt.
* En URL er ét ord på et par hundrede tegn uden et mellemrum i, og den kan derfor
* ikke ombryde af sig selv. Det gav to forskellige fejl på den samme side, og
* prøven her holder øje med dem begge:
*
* Abonnementets adresse lå i en liste uden noget, der lod den bryde, og det
* bredeste element på en side bestemmer sidens bredde — så hele indholdsspalten fik
* et vandret rullepanel, ikke bare den ene linje.
*
* Feed'et, krogen og CalDAV-serveren lå i skrivebeskyttede <input>, som ruller
* indeni i stedet for at ombryde. Der var ikke noget rullepanel at se: adressen
* stoppede bare midt i kassen, og resten fandtes kun ved at trække inde i feltet.
*
* Abonnementet kommer fra et svar, prøven selv skriver. Serveren ville skulle hente
* kalenderen for at have et at vise, og en prøve, der går på nettet, er en prøve,
* der fejler en dag af en anden grund end sin egen.
*/
test('en lang kalenderadresse giver ikke vandret rulning', async ({ page }) => {
test('adresserne på integrationssiden kan ses helt på en telefon', async ({ page }) => {
const trouble = watchForTrouble(page);
await page.route('**/api/v1/calendar', (route) =>
@@ -4421,5 +4427,30 @@ test('en lang kalenderadresse giver ikke vandret rulning', async ({ page }) => {
expect(spill.content).toBe(0);
expect(spill.past).toBeLessThanOrEqual(0);
// Og adresserne på siden står der helt. De lå i skrivebeskyttede <input>, som
// ikke kan ombryde: kassen var 306 px og indholdet 595, så resten af adressen
// fandtes kun ved at rulle inde i feltet — hvilket ingen opdager. Et felt, der
// er klippet, er et felt, hvor scrollWidth er større end clientWidth, og det er
// præcis det, der måles her.
//
// Felterne står ved navn og ikke som en forespørgsel på en klasse: en prøve, der
// måler `.field output` og finder nul, består — også den dag felterne er lavet om
// til noget andet klippet. Den skal fejle, hvis et af dem forsvinder.
const clipped = await page.evaluate(() =>
['feed', 'mail', 'hook', 'hookcurl', 'caldav']
.map((id) => {
const el = document.getElementById(id);
if (!el) return `${id}: findes ikke`;
if (el.scrollWidth - el.clientWidth > 1) return `${id}: ${el.scrollWidth} > ${el.clientWidth}`;
return null;
})
.filter(Boolean)
);
expect(clipped).toEqual([]);
// Selve teksten, ikke kun kassens mål: krogens adresse er den, der skal kunne
// læses af, hvis man taster den ind i en genvej på telefonen.
await expect(page.locator('#hook')).toHaveText(/^https?:\/\/.+\/inbound\/hook\/.+/);
expect(trouble).toEqual([]);
});
+18 -1
View File
@@ -151,7 +151,8 @@
.settings :global(.field input),
.settings :global(.field select),
.settings :global(.field textarea) {
.settings :global(.field textarea),
.settings :global(.field output) {
width: 100%;
padding: var(--s2) var(--s3);
background: var(--surface-sunken);
@@ -168,6 +169,22 @@
border-color: var(--accent);
}
/* Adresserne — feed'et, krogen, CalDAV-serveren — vises i et <output> og ikke i
et skrivebeskyttet <input>. Et input kan ikke ombryde: det ruller indeni, så
på en telefon står halvdelen af adressen uden for kassen, og der er ingen måde
at se resten på. <output> er stadig et felt, en <label for> kan pege på, og det
er tekst, der ombryder.
`user-select: all` er der, fordi det var det eneste, inputtet gjorde bedre: ét
tryk tog hele adressen. Uden den skulle man trække hen over fire linjer. */
.settings :global(.field output) {
display: block;
white-space: pre-wrap;
overflow-wrap: anywhere;
user-select: all;
line-height: 1.5;
}
/* The field error sits under its own input rather than in a summary at the top:
the server answers with a map of field to message, and putting each one where
it belongs is the whole point of that shape. */
@@ -460,7 +460,7 @@
it has to match Google's copy exactly, and the error Google gives
when it does not names neither value. Google afviser desuden
private IP-adresser over http — det skal være https. -->
<input id="redirect" readonly value={client.redirect_uri} />
<output id="redirect">{client.redirect_uri}</output>
</div>
<form onsubmit={saveClient}>
@@ -546,7 +546,7 @@
{#if app.user?.is_admin}
<div class="field">
<label for="calendar-redirect">{t('int.redirectURI')}</label>
<input id="calendar-redirect" class="mono" readonly value={calendar.redirect_uri} />
<output id="calendar-redirect" class="mono">{calendar.redirect_uri}</output>
<p class="hint">{t('int.calendarSameClient')}</p>
</div>
{:else if !calendar.has_client}
@@ -653,7 +653,7 @@
<div class="field">
<label for="feed">{t('int.address')}</label>
<input id="feed" class="mono" value={feedURL} readonly />
<output id="feed" class="mono">{feedURL}</output>
</div>
<div class="row">
@@ -681,7 +681,7 @@
<div class="field">
<label for="mail">{t('int.yourAddress')}</label>
<input id="mail" class="mono" value={mailAddress} readonly />
<output id="mail" class="mono">{mailAddress}</output>
</div>
<div class="row">
@@ -700,7 +700,7 @@
<div class="field">
<label for="hook">{t('int.yourHook')}</label>
<input id="hook" class="mono" value={hookURL} readonly />
<output id="hook" class="mono">{hookURL}</output>
<p class="hint">
{t('int.hookHow')}
</p>
@@ -711,8 +711,8 @@
kroppen bare er den linje, opgaven skal hedde. -->
<div class="field">
<label for="hookcurl">{t('int.hookExample')}</label>
<textarea id="hookcurl" class="mono" rows="3" readonly
>{`curl -X POST ${hookURL} \\\n -H "Content-Type: text/plain" \\\n -d "Ring til Anders i morgen p1 #Firma"`}</textarea
<output id="hookcurl" class="mono"
>{`curl -X POST ${hookURL} \\\n -H "Content-Type: text/plain" \\\n -d "Ring til Anders i morgen p1 #Firma"`}</output
>
</div>
@@ -732,7 +732,7 @@
<div class="field">
<label for="caldav">{t('int.server')}</label>
<input id="caldav" class="mono" value={`${location.origin}/caldav/`} readonly />
<output id="caldav" class="mono">{`${location.origin}/caldav/`}</output>
<p class="hint">
{t('int.caldavAuth')}
</p>
@@ -817,7 +817,7 @@
color: var(--ink-muted);
}
.setup input[readonly] {
.setup output {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--ink-muted);
+15 -3
View File
@@ -1279,7 +1279,7 @@
lovet i en besked, der ikke kom af sted. -->
<p class="invitelink">
{t('notes.inviteNoMail')}
<input readonly value={shareInviteLink} onclick={(e) => e.target.select()} />
<output>{shareInviteLink}</output>
</p>
{/if}
</section>
@@ -2216,11 +2216,23 @@
line-height: 1.5;
}
.invitelink input {
/* <output> og ikke et skrivebeskyttet <input>: et input kan ikke ombryde, så et
invitationslink stod halvt uden for kassen på en telefon, og resten kunne kun
nås ved at rulle inde i feltet. `user-select: all` beholder det, inputtet var
god til — ét tryk tager hele linket. */
.invitelink output {
display: block;
width: 100%;
margin-top: 2px;
font-size: var(--text-xs);
padding: 2px var(--s1);
background: var(--surface-sunken);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--ink);
overflow-wrap: anywhere;
user-select: all;
}
.addshare {