|
|
|
@@ -38,6 +38,61 @@ async function projectAction(page, name) {
|
|
|
|
|
await item.click();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Venter på, at en gemning faktisk er nået frem til serveren.
|
|
|
|
|
*
|
|
|
|
|
* 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 gemt(side, sti) {
|
|
|
|
|
return side.waitForResponse(
|
|
|
|
|
(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) => {
|
|
|
|
@@ -1683,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
|
|
|
|
@@ -1690,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();
|
|
|
|
@@ -1703,8 +1764,21 @@ test('en gruppe er en side med sine projekter, en beskrivelse og filer', async (
|
|
|
|
|
const about = page.getByLabel('Om gruppen');
|
|
|
|
|
await expect(about).toBeVisible();
|
|
|
|
|
await about.fill('Alt der larmer og støver.');
|
|
|
|
|
|
|
|
|
|
// Gemningen skal være nået frem, FØR der genindlæses. Beskrivelsen tegnes
|
|
|
|
|
// optimistisk — siden sætter teksten på gruppen med det samme og sender PATCH'en
|
|
|
|
|
// bagefter — så en kontrol på teksten er grøn, før serveren har hørt om den. En
|
|
|
|
|
// reload i det hul henter den gamle gruppe tilbage, og prøven fejler på noget,
|
|
|
|
|
// der ikke er i stykker. Svaret er det, der kan ventes på.
|
|
|
|
|
const gemt = page.waitForResponse(
|
|
|
|
|
(r) =>
|
|
|
|
|
r.request().method() === 'PATCH' &&
|
|
|
|
|
new URL(r.url()).pathname.includes('/project-groups/') &&
|
|
|
|
|
r.ok()
|
|
|
|
|
);
|
|
|
|
|
await about.blur();
|
|
|
|
|
await expect(page.getByText('Alt der larmer og støver.')).toBeVisible();
|
|
|
|
|
await gemt;
|
|
|
|
|
|
|
|
|
|
// And it survives a reload, which is the difference between the page keeping up
|
|
|
|
|
// and the server taking it.
|
|
|
|
@@ -2724,7 +2798,9 @@ test('noteeditoren er rich text, og teksten overlever turen til Markdown og tilb
|
|
|
|
|
// And the round trip, which is the thing that can quietly ruin a note: what is
|
|
|
|
|
// stored is Markdown, and reopening it has to give back the same document. A
|
|
|
|
|
// converter that disagrees with itself reshapes a note a little on every save.
|
|
|
|
|
const rundtur = noteGemt(page);
|
|
|
|
|
await page_.blur();
|
|
|
|
|
await rundtur;
|
|
|
|
|
await page.reload();
|
|
|
|
|
await page.getByRole('button', { name: /Møde med Anders/ }).click();
|
|
|
|
|
|
|
|
|
@@ -2911,8 +2987,9 @@ test('note links i en delt note fører til den rigtige note, eller siger hvorfor
|
|
|
|
|
await body.click();
|
|
|
|
|
await gæst.keyboard.press('Control+End');
|
|
|
|
|
await gæst.keyboard.type('\n[[Linneas egne maal]]\n[[Noget der aldrig blev skrevet]]');
|
|
|
|
|
const gemt = noteGemt(gæst);
|
|
|
|
|
await body.blur();
|
|
|
|
|
await expect(gæst.locator('footer .hint')).toHaveText('Gemt');
|
|
|
|
|
await gemt;
|
|
|
|
|
|
|
|
|
|
await gæst.goto(noterURL);
|
|
|
|
|
await gæst.locator('.notes button.row').filter({ hasText: /^Faelles aftale/ }).click();
|
|
|
|
@@ -2975,10 +3052,33 @@ test('en delt note tager de noter med, den peger på', async ({ browser, page })
|
|
|
|
|
'Aftale om levering\nse [[Prisliste i kroner]]'
|
|
|
|
|
]) {
|
|
|
|
|
await page.getByRole('button', { name: 'Ny note' }).click();
|
|
|
|
|
await page.getByRole('textbox', { name: 'Notens tekst' }).click();
|
|
|
|
|
const felt = page.getByRole('textbox', { name: 'Notens tekst' });
|
|
|
|
|
// Tomt felt før der skrives, og rækken i listen bagefter. IKKE "Gemt" i
|
|
|
|
|
// foden: den tekst er hvilestanden — der står "Gemmer" mens en gemning
|
|
|
|
|
// kører og "Gemt" ellers, altså også før den første er begyndt. En ventetid
|
|
|
|
|
// på den er grøn med det samme og venter på ingenting.
|
|
|
|
|
await expect(felt).toHaveText('');
|
|
|
|
|
await felt.click();
|
|
|
|
|
await page.keyboard.type(text);
|
|
|
|
|
await page.getByRole('textbox', { name: 'Notens tekst' }).blur();
|
|
|
|
|
await expect(page.locator('footer .hint')).toHaveText('Gemt');
|
|
|
|
|
await felt.blur();
|
|
|
|
|
const titel = text.split('\n')[0];
|
|
|
|
|
// Rækkens egen titel, og kun den. To fælder ligger på vejen hertil, og jeg
|
|
|
|
|
// gik i begge:
|
|
|
|
|
//
|
|
|
|
|
// `filter({ hasText: 'Aftale om levering' })` med en STRENG matcher
|
|
|
|
|
// delstreng og uden hensyn til store og små bogstaver — den fandt også
|
|
|
|
|
// "Fælles aftale om leveringen" fra en tidligere prøve, for databasen deles
|
|
|
|
|
// af hele suiten.
|
|
|
|
|
//
|
|
|
|
|
// `.notes` alene rækker for langt: en `[[henvisning]]` i editoren er også
|
|
|
|
|
// tekst med den titel i, så et opslag på teksten fandt tre.
|
|
|
|
|
//
|
|
|
|
|
// `button.row strong` er rækkens titelelement, og en forankret regex er både
|
|
|
|
|
// eksakt og versalfølsom. Titlerne her er almindelige ord uden
|
|
|
|
|
// regex-tegn — står der en dag en parentes i en af dem, skal den undviges.
|
|
|
|
|
await expect(
|
|
|
|
|
page.locator('.notes button.row strong').filter({ hasText: new RegExp(`^${titel}$`) })
|
|
|
|
|
).toHaveCount(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await page.locator('.notes button.row').filter({ hasText: /^Aftale om levering/ }).click();
|
|
|
|
@@ -3064,8 +3164,9 @@ test('</> sætter markeringen som kode, og kilden har sin egen knap', async ({ p
|
|
|
|
|
|
|
|
|
|
// Og den anden knap gør stadig det, den altid har gjort: viser filen, med
|
|
|
|
|
// baktikkerne i, som er beviset på at koden nåede hele vejen ned.
|
|
|
|
|
const kildeGemt = noteGemt(page);
|
|
|
|
|
await body.blur();
|
|
|
|
|
await expect(page.locator('footer .hint')).toHaveText('Gemt');
|
|
|
|
|
await kildeGemt;
|
|
|
|
|
await page.getByRole('button', { name: /Vis kilden/ }).click();
|
|
|
|
|
await expect(page.locator('textarea.source')).toHaveValue(/`/);
|
|
|
|
|
|
|
|
|
@@ -3137,8 +3238,11 @@ test('[[ foreslår en note, og linket lander i noten', async ({ page }) => {
|
|
|
|
|
await expect(inline).toBeVisible();
|
|
|
|
|
|
|
|
|
|
// It survives the round-trip through Markdown and a reload.
|
|
|
|
|
// Gemningen, ikke en ventetid på 900 ms. Tallet var gættet til at overleve
|
|
|
|
|
// tastepausen på 700 ms, og et gæt om tid måler maskinen frem for programmet.
|
|
|
|
|
const linketGemt = noteGemt(page);
|
|
|
|
|
await ed.blur();
|
|
|
|
|
await page.waitForTimeout(900);
|
|
|
|
|
await linketGemt;
|
|
|
|
|
await page.reload();
|
|
|
|
|
await page.getByRole('button', { name: /Mine planer/ }).click();
|
|
|
|
|
await expect(ed.locator('a.notelink', { hasText: 'Rejseplan til Berlin' })).toBeVisible();
|
|
|
|
@@ -3675,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');
|
|
|
|
|
|
|
|
|
@@ -3713,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();
|
|
|
|
@@ -3722,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');
|
|
|
|
@@ -4025,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(
|
|
|
|
@@ -4748,6 +4851,78 @@ test('en ny note hentes ikke igen, når den lige er lavet', async ({ page }) =>
|
|
|
|
|
expect(trouble).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Et totalt udfald siger "ingen forbindelse" — ikke "log ind".
|
|
|
|
|
*
|
|
|
|
|
* Opstarten spørger serveren, hvem man er. Når svaret aldrig kommer, står `user`
|
|
|
|
|
* som null, og null betyder "logget ud" alle andre steder i programmet — så et
|
|
|
|
|
* udfald blev vist som login-skærmen. Appen gav altså sessionen skylden for, at
|
|
|
|
|
* nettet var nede, og tilbød et kodeordsfelt, der umuligt kunne virke.
|
|
|
|
|
*
|
|
|
|
|
* Det er værre end en fejlbesked, fordi det ligner noget, der kunne virke: man
|
|
|
|
|
* taster kodeordet, der sker ingenting, og man konkluderer, at kontoen er i
|
|
|
|
|
* stykker. Det skete i virkeligheden 17. september 2026, da instansens DNS-post
|
|
|
|
|
* blev slettet.
|
|
|
|
|
*
|
|
|
|
|
* Prøven afbryder API-kaldene frem for at stoppe serveren: en afbrudt forespørgsel
|
|
|
|
|
* er præcis det, en manglende DNS-post giver browseren — fetch afviser, uden at der
|
|
|
|
|
* nogensinde kom en status. En 500 ville være serveren, der svarer, og det er den
|
|
|
|
|
* anden sag.
|
|
|
|
|
*/
|
|
|
|
|
test('et udfald siger ingen forbindelse frem for at bede om kodeordet', async ({ page }) => {
|
|
|
|
|
await page.route('**/api/v1/**', (route) => route.abort());
|
|
|
|
|
|
|
|
|
|
await page.goto('/');
|
|
|
|
|
|
|
|
|
|
// Prøvetiden skal kunne rumme hele forsøgsvinduet: forespørgselslaget prøver
|
|
|
|
|
// igen i femten sekunder, før det kalder det et udfald. Det er med vilje —
|
|
|
|
|
// et blink på nettet må ikke smide folk ud — så prøven må vente det ud.
|
|
|
|
|
await expect(page.getByText('Ingen forbindelse', { exact: true })).toBeVisible({
|
|
|
|
|
timeout: 25000
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Og ikke kodeordsfeltet. Det er hele pointen.
|
|
|
|
|
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.
|
|
|
|
|
*
|
|
|
|
@@ -4903,7 +5078,7 @@ test('søgefeltet taber ikke bogstaver, og det nyeste svar vinder', async ({ pag
|
|
|
|
|
const krop = page.getByLabel('Notens tekst');
|
|
|
|
|
await krop.fill('Kaffemølle\n\nnoget om den');
|
|
|
|
|
await krop.blur();
|
|
|
|
|
await expect(page.getByText('Gemt', { exact: true })).toBeVisible();
|
|
|
|
|
// Rækken i listen er værnet. "Gemt" stod her før og er hvilestanden — se noteGemt().
|
|
|
|
|
await expect(page.locator('.list').getByText('Kaffemølle', { exact: true })).toBeVisible();
|
|
|
|
|
|
|
|
|
|
// Skrevet i et tempo, et menneske kan skrive i. Det var hastigheden, der
|
|
|
|
|