SurfSense/.cursor/skills/playwright-testing/advanced/network-advanced.md
2026-05-10 04:19:55 +05:30

11 KiB

Advanced Network Interception

Table of Contents

  1. Request Modification
  2. GraphQL Mocking
  3. HAR Recording & Playback
  4. Conditional Mocking
  5. Network Throttling

Request Modification

Modify Request Headers

test("add auth header to requests", async ({ page }) => {
  await page.route("**/api/**", (route) => {
    const headers = {
      ...route.request().headers(),
      Authorization: "Bearer test-token",
      "X-Test-Header": "test-value",
    };
    route.continue({ headers });
  });

  await page.goto("/dashboard");
});

Modify Request Body

test("modify POST body", async ({ page }) => {
  await page.route("**/api/orders", async (route) => {
    if (route.request().method() === "POST") {
      const postData = route.request().postDataJSON();

      // Add test metadata
      const modifiedData = {
        ...postData,
        testMode: true,
        testTimestamp: Date.now(),
      };

      await route.continue({
        postData: JSON.stringify(modifiedData),
      });
    } else {
      await route.continue();
    }
  });

  await page.goto("/checkout");
  await page.getByRole("button", { name: "Place Order" }).click();
});

Transform Response

test("modify API response", async ({ page }) => {
  await page.route("**/api/products", async (route) => {
    // Fetch real response
    const response = await route.fetch();
    const json = await response.json();

    // Modify response
    const modified = json.map((product: any) => ({
      ...product,
      price: product.price * 0.9, // 10% discount
      testMode: true,
    }));

    await route.fulfill({
      response,
      json: modified,
    });
  });

  await page.goto("/products");
});

GraphQL Mocking

Mock by Operation Name

test("mock GraphQL query", async ({ page }) => {
  await page.route("**/graphql", async (route) => {
    const postData = route.request().postDataJSON();

    if (postData.operationName === "GetUser") {
      return route.fulfill({
        json: {
          data: {
            user: {
              id: "1",
              name: "Test User",
              email: "test@example.com",
            },
          },
        },
      });
    }

    if (postData.operationName === "GetProducts") {
      return route.fulfill({
        json: {
          data: {
            products: [
              { id: "1", name: "Product A", price: 29.99 },
              { id: "2", name: "Product B", price: 49.99 },
            ],
          },
        },
      });
    }

    // Pass through unmocked operations
    return route.continue();
  });

  await page.goto("/dashboard");
});

GraphQL Mock Fixture

// fixtures/graphql.fixture.ts
type GraphQLMock = {
  operation: string;
  variables?: Record<string, any>;
  response: { data?: any; errors?: any[] };
};

type GraphQLFixtures = {
  mockGraphQL: (mocks: GraphQLMock[]) => Promise<void>;
};

export const test = base.extend<GraphQLFixtures>({
  mockGraphQL: async ({ page }, use) => {
    await use(async (mocks) => {
      await page.route("**/graphql", async (route) => {
        const postData = route.request().postDataJSON();

        const mock = mocks.find((m) => {
          if (m.operation !== postData.operationName) return false;

          // Optionally match variables
          if (m.variables) {
            return (
              JSON.stringify(m.variables) === JSON.stringify(postData.variables)
            );
          }
          return true;
        });

        if (mock) {
          return route.fulfill({ json: mock.response });
        }

        return route.continue();
      });
    });
  },
});

// Usage
test("dashboard with mocked GraphQL", async ({ page, mockGraphQL }) => {
  await mockGraphQL([
    {
      operation: "GetDashboardStats",
      response: {
        data: { stats: { users: 100, revenue: 50000 } },
      },
    },
    {
      operation: "GetUser",
      variables: { id: "1" },
      response: {
        data: { user: { id: "1", name: "John" } },
      },
    },
  ]);

  await page.goto("/dashboard");
  await expect(page.getByText("100 users")).toBeVisible();
});

Mock GraphQL Mutations

test("mock GraphQL mutation", async ({ page }) => {
  await page.route("**/graphql", async (route) => {
    const postData = route.request().postDataJSON();

    if (postData.operationName === "CreateOrder") {
      const { input } = postData.variables;

      return route.fulfill({
        json: {
          data: {
            createOrder: {
              id: "order-123",
              status: "PENDING",
              items: input.items,
              total: input.items.reduce(
                (sum: number, item: any) => sum + item.price * item.quantity,
                0,
              ),
            },
          },
        },
      });
    }

    return route.continue();
  });

  await page.goto("/checkout");
  await page.getByRole("button", { name: "Place Order" }).click();

  await expect(page.getByText("Order #order-123")).toBeVisible();
});

HAR Recording & Playback

Record HAR File

// Record network traffic
test("record HAR", async ({ page, context }) => {
  // Start recording
  await context.routeFromHAR("./recordings/checkout.har", {
    update: true, // Create/update HAR file
    url: "**/api/**",
  });

  await page.goto("/checkout");
  await page.getByRole("button", { name: "Place Order" }).click();

  // HAR file is saved automatically
});

Playback HAR File

// Use recorded HAR for offline testing
test("playback HAR", async ({ page, context }) => {
  await context.routeFromHAR("./recordings/checkout.har", {
    url: "**/api/**",
    update: false, // Don't update, just playback
  });

  await page.goto("/checkout");

  // All API calls served from HAR file
  await expect(page.getByText("Order confirmed")).toBeVisible();
});

HAR with Fallback

test("HAR with live fallback", async ({ page, context }) => {
  await context.routeFromHAR("./recordings/api.har", {
    url: "**/api/**",
    update: false,
    notFound: "fallback", // Use real network if not in HAR
  });

  await page.goto("/dashboard");
});

Conditional Mocking

Mock Based on Request Body

test("conditional mock by body", async ({ page }) => {
  await page.route("**/api/search", async (route) => {
    const body = route.request().postDataJSON();

    if (body.query === "error") {
      return route.fulfill({
        status: 500,
        json: { error: "Search failed" },
      });
    }

    if (body.query === "empty") {
      return route.fulfill({
        json: { results: [] },
      });
    }

    // Default response
    return route.fulfill({
      json: {
        results: [{ id: 1, title: `Result for: ${body.query}` }],
      },
    });
  });

  await page.goto("/search");

  // Test different scenarios
  await page.getByLabel("Search").fill("error");
  await page.getByLabel("Search").press("Enter");
  await expect(page.getByText("Search failed")).toBeVisible();
});

Mock Nth Request

test("different response on retry", async ({ page }) => {
  let callCount = 0;

  await page.route("**/api/status", (route) => {
    callCount++;

    if (callCount < 3) {
      return route.fulfill({
        status: 503,
        json: { error: "Service unavailable" },
      });
    }

    // Succeed on 3rd attempt
    return route.fulfill({
      json: { status: "ok" },
    });
  });

  await page.goto("/dashboard");

  // App should retry and eventually succeed
  await expect(page.getByText("Connected")).toBeVisible();
});

Mock with Delay

test("slow network simulation", async ({ page }) => {
  await page.route("**/api/data", async (route) => {
    // Simulate 2 second delay
    await new Promise((resolve) => setTimeout(resolve, 2000));

    return route.fulfill({
      json: { data: "loaded" },
    });
  });

  await page.goto("/dashboard");

  // Loading state should appear
  await expect(page.getByText("Loading...")).toBeVisible();

  // Then data appears
  await expect(page.getByText("loaded")).toBeVisible();
});

Network Throttling

Slow 3G Simulation

test("slow network experience", async ({ page, context }) => {
  // Create CDP session for network throttling
  const client = await context.newCDPSession(page);

  await client.send("Network.emulateNetworkConditions", {
    offline: false,
    downloadThroughput: (500 * 1024) / 8, // 500 Kbps
    uploadThroughput: (500 * 1024) / 8,
    latency: 400, // 400ms
  });

  await page.goto("/");

  // Test loading states appear
  await expect(page.getByTestId("skeleton-loader")).toBeVisible();
});

Offline Mode

Use context.setOffline(true/false) to simulate network connectivity changes.

For comprehensive offline testing patterns:

  • Network failure simulation (error recovery, graceful degradation): See error-testing.md
  • Offline-first/PWA testing (service workers, caching, background sync): See service-workers.md

Network Throttling Fixture

// fixtures/network.fixture.ts
type NetworkCondition = "slow3g" | "fast3g" | "offline";

const conditions = {
  slow3g: { downloadThroughput: 50000, uploadThroughput: 50000, latency: 2000 },
  fast3g: { downloadThroughput: 180000, uploadThroughput: 75000, latency: 150 },
};

type NetworkFixtures = {
  setNetworkCondition: (condition: NetworkCondition) => Promise<void>;
};

export const test = base.extend<NetworkFixtures>({
  setNetworkCondition: async ({ page, context }, use) => {
    const client = await context.newCDPSession(page);

    await use(async (condition) => {
      if (condition === "offline") {
        await context.setOffline(true);
      } else {
        await client.send("Network.emulateNetworkConditions", {
          offline: false,
          ...conditions[condition],
        });
      }
    });

    // Reset
    await context.setOffline(false);
  },
});

Anti-Patterns to Avoid

Anti-Pattern Problem Solution
Mocking all requests Tests don't reflect reality Mock only what's necessary
No cleanup of routes Routes persist across tests Use fixtures with cleanup
Ignoring request method Mock applies to wrong requests Check route.request().method()
Hardcoded mock responses Brittle, hard to maintain Use factories for mock data