Add 4 Garmin E2E tests for network error recovery
All checks were successful
Deploy / deploy (push) Successful in 2m28s

Add tests to verify error handling when network requests fail:
- Error toast when token save fails (500 response)
- Error toast when disconnect fails (500 response)
- Error state display when status fetch fails
- Retry succeeds after network failure

These tests improve resilience coverage for the Garmin connection flow.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-13 18:54:58 +00:00
parent b6f139883f
commit 04a532bb01
2 changed files with 171 additions and 8 deletions

View File

@@ -368,5 +368,171 @@ test.describe("garmin connection", () => {
const greenIndicator = page.locator(".bg-green-500").first();
await expect(greenIndicator).toBeVisible();
});
test("shows error toast when network fails during token save", async ({
page,
}) => {
// Intercept the POST request and simulate network failure
await page.route("**/api/garmin/tokens", (route) => {
if (route.request().method() === "POST") {
route.fulfill({
status: 500,
body: JSON.stringify({ error: "Internal server error" }),
});
} else {
route.continue();
}
});
// Enter valid tokens
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 90);
const validTokens = JSON.stringify({
oauth1: { token: "test-token-network", secret: "test-secret" },
oauth2: { access_token: "test-access-token" },
expires_at: expiresAt.toISOString(),
});
const tokenInput = page.locator("#tokenInput");
await tokenInput.fill(validTokens);
await page.getByRole("button", { name: /save tokens/i }).click();
// Error toast should appear
const errorToast = page.getByText(/internal server error/i);
await expect(errorToast).toBeVisible({ timeout: 5000 });
// Token input should still be visible for retry (this is the key behavior)
await expect(tokenInput).toBeVisible();
// Should NOT show success - either "Not Connected" or "Token Expired" state
// (depends on prior test state), but definitely not "Connected" without expiry
const connectedWithoutExpiry =
(await page.getByText("Connected", { exact: true }).isVisible()) &&
!(await page.getByText(/token expired/i).isVisible());
expect(connectedWithoutExpiry).toBe(false);
});
test("shows error toast when network fails during disconnect", async ({
page,
}) => {
// First connect successfully
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 90);
const validTokens = JSON.stringify({
oauth1: { token: "test-token-disconnect-error", secret: "test-secret" },
oauth2: { access_token: "test-access-token" },
expires_at: expiresAt.toISOString(),
});
const tokenInput = page.locator("#tokenInput");
await tokenInput.fill(validTokens);
await page.getByRole("button", { name: /save tokens/i }).click();
// Wait for connected state
await expect(page.getByText("Connected", { exact: true })).toBeVisible({
timeout: 10000,
});
// Now intercept DELETE request to simulate network failure
await page.route("**/api/garmin/tokens", (route) => {
if (route.request().method() === "DELETE") {
route.fulfill({
status: 500,
body: JSON.stringify({ error: "Failed to disconnect" }),
});
} else {
route.continue();
}
});
// Click disconnect
await page.getByRole("button", { name: /disconnect/i }).click();
// Error toast should appear
const errorToast = page.getByText(/failed to disconnect/i);
await expect(errorToast).toBeVisible({ timeout: 5000 });
// Should still show connected state (disconnect failed)
await expect(page.getByText("Connected", { exact: true })).toBeVisible();
});
test("shows error state when status fetch fails", async ({ page }) => {
// Intercept status fetch to simulate network failure
await page.route("**/api/garmin/status", (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: "Service unavailable" }),
});
});
// Navigate to garmin settings (need to re-navigate to trigger fresh fetch)
await page.goto("/settings/garmin");
await page.waitForLoadState("networkidle");
// Error alert should be visible (use specific text to avoid matching route announcer)
const errorAlert = page.getByText("Service unavailable");
await expect(errorAlert).toBeVisible({ timeout: 10000 });
// Error toast should also appear
const errorToast = page.getByText(/unable to fetch data/i);
await expect(errorToast).toBeVisible({ timeout: 5000 });
});
test("can retry and succeed after network failure", async ({ page }) => {
let requestCount = 0;
// First request fails, subsequent requests succeed
await page.route("**/api/garmin/tokens", (route) => {
if (route.request().method() === "POST") {
requestCount++;
if (requestCount === 1) {
// First attempt fails
route.fulfill({
status: 500,
body: JSON.stringify({ error: "Temporary failure" }),
});
} else {
// Subsequent attempts succeed
route.fulfill({
status: 200,
body: JSON.stringify({ success: true }),
});
}
} else {
route.continue();
}
});
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 90);
const validTokens = JSON.stringify({
oauth1: { token: "test-token-retry", secret: "test-secret" },
oauth2: { access_token: "test-access-token" },
expires_at: expiresAt.toISOString(),
});
const tokenInput = page.locator("#tokenInput");
await tokenInput.fill(validTokens);
// First attempt - should fail
await page.getByRole("button", { name: /save tokens/i }).click();
// Error toast should appear
const errorToast = page.getByText(/temporary failure/i);
await expect(errorToast).toBeVisible({ timeout: 5000 });
// Wait for toast to disappear or proceed with retry
await page.waitForTimeout(1000);
// Retry - should succeed now
await page.getByRole("button", { name: /save tokens/i }).click();
// Success toast should appear
const successToast = page.getByText(/tokens saved successfully/i);
await expect(successToast).toBeVisible({ timeout: 10000 });
});
});
});