mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
9.5 KiB
9.5 KiB
Error & Edge Case Testing
Table of Contents
Error Boundaries
Test Component Errors
test("error boundary catches component error", async ({ page }) => {
// Trigger error via mock
await page.route("**/api/user", (route) => {
route.fulfill({
json: null, // Will cause component to throw
});
});
await page.goto("/profile");
// Error boundary should render fallback
await expect(page.getByText("Something went wrong")).toBeVisible();
await expect(page.getByRole("button", { name: "Try Again" })).toBeVisible();
});
Test Error Recovery
test("recover from error state", async ({ page }) => {
let requestCount = 0;
await page.route("**/api/data", (route) => {
requestCount++;
if (requestCount === 1) {
return route.fulfill({ status: 500 });
}
return route.fulfill({
json: { data: "success" },
});
});
await page.goto("/dashboard");
// Error state
await expect(page.getByText("Failed to load")).toBeVisible();
// Retry
await page.getByRole("button", { name: "Retry" }).click();
// Success state
await expect(page.getByText("success")).toBeVisible();
});
Test JavaScript Errors
test("handles runtime error gracefully", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => {
errors.push(error.message);
});
await page.goto("/buggy-page");
// App should still be functional despite error
await expect(page.getByRole("navigation")).toBeVisible();
// Error was logged
expect(errors.length).toBeGreaterThan(0);
});
Network Failures
Test API Errors
test.describe("API error handling", () => {
const errorCodes = [400, 401, 403, 404, 500, 502, 503];
for (const status of errorCodes) {
test(`handles ${status} error`, async ({ page }) => {
await page.route("**/api/data", (route) =>
route.fulfill({
status,
json: { error: `Error ${status}` },
}),
);
await page.goto("/dashboard");
// Appropriate error message shown
await expect(page.getByRole("alert")).toBeVisible();
});
}
});
Test Timeout
test("handles request timeout", async ({ page }) => {
await page.route("**/api/slow", async (route) => {
// Never respond - simulates timeout
await new Promise(() => {});
});
await page.goto("/slow-page");
// Should show timeout message (app should have its own timeout)
await expect(page.getByText("Request timed out")).toBeVisible({
timeout: 15000,
});
});
Test Connection Reset
test("handles connection failure", async ({ page }) => {
await page.route("**/api/data", (route) => {
route.abort("connectionfailed");
});
await page.goto("/dashboard");
await expect(page.getByText("Connection failed")).toBeVisible();
await expect(page.getByRole("button", { name: "Retry" })).toBeVisible();
});
Test Mid-Request Failure
test("handles failure during request", async ({ page }) => {
let requestStarted = false;
await page.route("**/api/upload", async (route) => {
requestStarted = true;
// Abort after small delay (mid-request)
await new Promise((resolve) => setTimeout(resolve, 500));
route.abort("failed");
});
await page.goto("/upload");
await page.getByLabel("File").setInputFiles("./fixtures/large-file.pdf");
await page.getByRole("button", { name: "Upload" }).click();
// Should show failure, not hang
await expect(page.getByText("Upload failed")).toBeVisible();
expect(requestStarted).toBe(true);
});
Offline Testing
This section covers unexpected network failures and error recovery. For offline-first apps (PWAs) with service workers, caching, and background sync, see service-workers.md.
Go Offline During Session
test("handles going offline", async ({ page, context }) => {
await page.goto("/dashboard");
await expect(page.getByTestId("data")).toBeVisible();
// Go offline unexpectedly
await context.setOffline(true);
// Try to refresh data
await page.getByRole("button", { name: "Refresh" }).click();
// Should show offline indicator
await expect(page.getByText("You're offline")).toBeVisible();
// Go back online
await context.setOffline(false);
// Should recover
await page.getByRole("button", { name: "Refresh" }).click();
await expect(page.getByText("You're offline")).toBeHidden();
});
Test Network Recovery
test("recovers gracefully when connection returns", async ({
page,
context,
}) => {
await page.goto("/dashboard");
// Simulate connection drop
await context.setOffline(true);
// App should show degraded state
await expect(page.getByRole("alert")).toContainText(/offline|connection/i);
// Connection restored
await context.setOffline(false);
// Retry should work
await page.getByRole("button", { name: "Retry" }).click();
await expect(page.getByTestId("data")).toBeVisible();
});
Loading States
Test Skeleton Loaders
test("shows skeleton during load", async ({ page }) => {
// Add delay to API response
await page.route("**/api/posts", async (route) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
route.fulfill({
json: [{ id: 1, title: "Post 1" }],
});
});
await page.goto("/posts");
// Skeleton should appear immediately
await expect(page.getByTestId("skeleton")).toBeVisible();
// Then content replaces skeleton
await expect(page.getByText("Post 1")).toBeVisible();
await expect(page.getByTestId("skeleton")).toBeHidden();
});
Test Loading Indicators
test("shows loading state for actions", async ({ page }) => {
await page.route("**/api/save", async (route) => {
await new Promise((resolve) => setTimeout(resolve, 500));
route.fulfill({ json: { success: true } });
});
await page.goto("/editor");
await page.getByLabel("Content").fill("New content");
const saveButton = page.getByRole("button", { name: "Save" });
await saveButton.click();
// Button should show loading state
await expect(saveButton).toBeDisabled();
await expect(page.getByTestId("spinner")).toBeVisible();
// Then success state
await expect(saveButton).toBeEnabled();
await expect(page.getByText("Saved")).toBeVisible();
});
Test Empty States
test("shows empty state when no data", async ({ page }) => {
await page.route("**/api/items", (route) => route.fulfill({ json: [] }));
await page.goto("/items");
await expect(page.getByText("No items yet")).toBeVisible();
await expect(
page.getByRole("button", { name: "Create First Item" }),
).toBeVisible();
});
Form Validation
Test Client-Side Validation
test("validates required fields", async ({ page }) => {
await page.goto("/signup");
// Submit empty form
await page.getByRole("button", { name: "Sign Up" }).click();
// Should show validation errors
await expect(page.getByText("Email is required")).toBeVisible();
await expect(page.getByText("Password is required")).toBeVisible();
// Form should not submit
await expect(page).toHaveURL("/signup");
});
Test Format Validation
test("validates email format", async ({ page }) => {
await page.goto("/signup");
await page.getByLabel("Email").fill("invalid-email");
await page.getByLabel("Email").blur();
await expect(page.getByText("Invalid email address")).toBeVisible();
// Fix the error
await page.getByLabel("Email").fill("valid@email.com");
await page.getByLabel("Email").blur();
await expect(page.getByText("Invalid email address")).toBeHidden();
});
Test Server-Side Validation
test("handles server validation errors", async ({ page }) => {
await page.route("**/api/register", (route) =>
route.fulfill({
status: 422,
json: {
errors: {
email: "Email already exists",
username: "Username is taken",
},
},
}),
);
await page.goto("/signup");
await page.getByLabel("Email").fill("taken@email.com");
await page.getByLabel("Username").fill("takenuser");
await page.getByLabel("Password").fill("password123");
await page.getByRole("button", { name: "Sign Up" }).click();
// Server errors should display
await expect(page.getByText("Email already exists")).toBeVisible();
await expect(page.getByText("Username is taken")).toBeVisible();
});
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Only testing happy path | Misses error handling bugs | Test all error scenarios |
| No network failure tests | App crashes on poor connection | Test offline/slow/failed requests |
| Skipping loading states | Janky UX not caught | Assert loading UI appears |
| Ignoring validation | Form bugs slip through | Test both client and server validation |
Related References
- Network Mocking: See network-advanced.md for mock patterns
- Assertions: See assertions-waiting.md for error assertions