feat(offline): ride out short connection drops instead of losing the save
A save made while the connection blips for a few seconds is retried rather than reverted: the request layer keeps trying a network failure for a fifteen-second window, waking the instant the OS reports a link again, and only falls back to the existing error path if the drop outlasts it. A quiet "reconnecting" pill appears if the wait passes about a second, and leaves the moment it heals — so a two-second blip is felt as nothing at all. This is deliberately not offline mode: the app is a live view of a shared list, so there is no cache to work in with no server and no write log to reconcile. It is only the blip, ridden out. A server error (4xx/5xx) is the server answering and is not retried; an aborted request (a superseded search) is left to propagate. Kristian: an offline function for when the net drops for short periods, seconds. Shared items stay server-authoritative — nothing is edited against a stale copy for longer than the blip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8dc601d09a
commit
b2fcc0b104
+53
-2
@@ -565,8 +565,13 @@ test('en opgave kan trækkes ned i en tom sektion, og en fejlet indlæsning sige
|
||||
await expect(section.getByText('brænde kaffe')).toBeVisible();
|
||||
await expect(page.locator('.toast')).toHaveCount(0);
|
||||
|
||||
// A load that fails says so, and offers to try again.
|
||||
await page.route('**/api/v1/projects/*', (r) => r.abort());
|
||||
// A load that fails with a server error says so at once and offers to try again.
|
||||
// A 500 is the server answering, not a dropped connection, so it is not a blip to
|
||||
// wait out — it is not retried, and the message is immediate. (A dropped
|
||||
// connection now retries for a few seconds first; that is the self-heal test.)
|
||||
await page.route('**/api/v1/projects/*', (r) =>
|
||||
r.fulfill({ status: 500, contentType: 'application/json', body: '{"code":"internal_error"}' })
|
||||
);
|
||||
await page.reload();
|
||||
await expect(page.getByText('Serveren svarede ikke')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Prøv igen' })).toBeVisible();
|
||||
@@ -577,6 +582,52 @@ test('en opgave kan trækkes ned i en tom sektion, og en fejlet indlæsning sige
|
||||
expect(trouble.filter((t) => !t.includes('projects'))).toEqual([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* A save made during a short drop is not lost to it.
|
||||
*
|
||||
* This is the whole of what "offline" means here: not a cache to work in with no
|
||||
* server, but a save that rides out a few seconds of no connection and goes through
|
||||
* when it comes back — with a quiet "reconnecting" while it waits, and no error and
|
||||
* no reverted change once it heals. A drop that outlasts the retry window is a real
|
||||
* outage and takes the normal error path; that is not this test.
|
||||
*/
|
||||
test('en kort netaudsætning under en handling genoprettes af sig selv', async ({ page }) => {
|
||||
const trouble = watchForTrouble(page);
|
||||
await page.goto('/');
|
||||
|
||||
// Wait for the app to finish loading before cutting the line — `goto` resolves at
|
||||
// the load event, but the data load runs after hydration, and aborting that would
|
||||
// test a cold start, not a blip mid-use.
|
||||
const box = page.getByLabel('Ny opgave');
|
||||
await expect(box).toBeVisible();
|
||||
|
||||
// The app is loaded and online. Now the connection drops — every call fails, the
|
||||
// way it does in a tunnel, rather than one endpoint erroring.
|
||||
let down = true;
|
||||
await page.route('**/api/v1/**', (route) => (down ? route.abort() : route.continue()));
|
||||
|
||||
// A task captured during the drop. Its save fails on the network and starts
|
||||
// retrying rather than giving up.
|
||||
await box.fill('under et udfald');
|
||||
await box.press('Enter');
|
||||
|
||||
// The quiet pill, once the retry has lasted long enough to be worth showing.
|
||||
await expect(page.locator('.netstatus')).toBeVisible();
|
||||
|
||||
// The drop ends. The retry goes through on its own and the pill leaves — no
|
||||
// error, nothing toasted as failed.
|
||||
down = false;
|
||||
await expect(page.locator('.netstatus')).toHaveCount(0, { timeout: 10000 });
|
||||
await expect(page.locator('.toast')).toHaveCount(0);
|
||||
|
||||
// And the save landed: an undated quick capture goes to the Inbox, and there it
|
||||
// is, saved across the drop rather than lost to it.
|
||||
await page.getByRole('link', { name: /Indbakke/ }).click();
|
||||
await expect(page.getByText('under et udfald')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
expect(trouble).toEqual([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* A task can be dragged onto another day, and onto another project.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user