feat(links): vælg hvilken browser et link åbner i

Ønsket var en liste over de installerede browsere. Den findes ikke: en
webside kan hverken se, hvilke browsere der er på maskinen — listen er
lukket land, fordi den er et fingeraftryk, der ville følge folk rundt på
nettet — eller bede systemet om at bruge en bestemt af dem.

Det eneste håndtag er de URL-skemaer, browserne selv registrerer:
firefox://open-url, googlechrome://, microsoft-edge:. Dem kan man prøve,
og man kan ikke spørge, om de findes. Så indstillingen er skrevet som
det, den er — et forsøg med et faldback — frem for som et løfte, der
ville holde på telefonen og ikke på en Mac.

Valget står i localStorage, ikke på kontoen. Samme grund som
sidebjælkens bredde: det er en egenskab ved maskinen, man sidder ved.
firefox:// virker på telefonen og gør ingenting på Mac'en, så et valg,
der fulgte kontoen, ville være forkert det ene af de to steder hver gang.

Ét sted i koden frem for ét pr. komponent. Links står i opgavetitler, i
beskrivelser, i noter og i indstillingerne, og en regel, der skulle
gentages hvert af de steder, ville mangle det ene sted, ingen tænkte på.
Kun udadgående links; programmets egne ruter går gennem SvelteKit som før.

To ting, målingen rettede undervejs:

Det skjulte iframe — den kendte opskrift — bliver blokeret af appens egen
Content-Security-Policy, som siger default-src 'self'. Uden e2e-prøven,
der fangede CSP-fejlen i konsollen, ville skemaet aldrig være blevet
forsøgt i produktion, og indstillingen ville have set ud til at virke,
fordi faldbacket åbnede linket alligevel. Nu sættes location i stedet:
et ukendt skema afbryder navigationen frem for at forlade siden.

Og faldbacket troede sig blokeret hver eneste gang. window.open med
noopener returnerer altid null — det står i specifikationen — så
kontrollen af, om fanen blev åbnet, faldt altid ud til "blokeret", og
den aktuelle fane blev navigeret væk i stedet. Håndtaget er det eneste,
der kan kende en blokeret fane fra en åbnet, så det bruges nu, og opener
ryddes bagefter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-09-09 08:41:21 +02:00
co-authored by Claude Opus 5
parent e931ddc313
commit 5eb689e4c2
6 changed files with 356 additions and 1 deletions
+53
View File
@@ -4272,3 +4272,56 @@ test('en gentagelse, serveren kun kan på dansk, siges alligevel i fladen', asyn
expect(trouble).toEqual([]);
});
test('links kan åbnes i en anden browser, og åbner alligevel når skemaet ikke findes', async ({
page
}) => {
const trouble = watchForTrouble(page);
await page.goto('/indstillinger');
await expect(page.getByRole('heading', { name: 'Links' })).toBeVisible();
// Som udgangspunkt gør indstillingen ingenting, og der er intet skemafelt at
// skrive i: et felt til en adresse, der ikke bliver brugt, er en invitation
// til at udfylde den og undre sig.
await expect(page.getByLabel('Skema')).toBeHidden();
// Et skema, der med sikkerhed ikke findes på nogen maskine. Pointen er ikke,
// at det virker — pointen er, hvad der sker, når det ikke gør.
await page.getByLabel('Åbn links i').selectOption('custom');
await page.getByLabel('Skema').fill('verdande-findes-ikke://{url}');
await page.getByLabel('Skema').blur();
// Valget hører til maskinen, ikke kontoen, så det står i localStorage.
const stored = await page.evaluate(() => localStorage.getItem('verdande:link-opener'));
expect(stored, 'valget blev ikke gemt på apparatet').toContain('verdande-findes-ikke');
// Om browseren rent faktisk tegner en fane, er browserens sag og ikke appens —
// en headless Chromium åbner den uden at fortælle nogen om det. Det, der er
// vores, er beslutningen: prøv skemaet, og åbn linket alligevel, når det ikke
// findes. Så det er dét, der bliver målt.
await page.evaluate(() => {
window.__opened = [];
window.open = (url) => {
window.__opened.push(url);
return { closed: false, close() {} };
};
});
await page.getByRole('button', { name: 'Prøv det' }).click();
await expect
.poll(() => page.evaluate(() => window.__opened), { timeout: 5000 })
.toEqual(['https://example.dk']);
// Og appen blev, hvor den var. Faldbacket må ikke kapre den fane, man står i:
// et link, der smider en ud af programmet, er værre end et, der åbner et sted,
// man ikke havde valgt.
await expect(page).toHaveURL(/\/indstillinger$/);
// Tilbage til systemets standard, så resten af prøverne klikker på links som
// alle andre.
await page.getByLabel('Åbn links i').selectOption('');
await expect(page.getByLabel('Skema')).toBeHidden();
expect(trouble).toEqual([]);
});