Converting request interception example from Cypress to Playwright Python

Question:

The Cypress project has a sample test on how to intercept and modify AJAX calls and I wanted to rewrite the same example in Playwright for Python (pytest).

The example consists on intercepting and modifying an Ajax call that feeds the Comments section of a webpage.

This is the Cypress code. The test successfully passes.

it('cy.intercept() - route responses to matching requests', () => {

    cy.visit('https://example.cypress.io/commands/network-requests')
    let message = 'whoa, this comment does not exist'

    // Stub a response to PUT comments/ ****
    cy.intercept({
      method: 'PUT',
      url: '**/comments/*',
    }, {
      statusCode: 404,
      body: { error: message },
      headers: { 'access-control-allow-origin': '*' },
      delayMs: 500,
    }).as('putComment')

    // we have code that puts a comment when the button is clicked in scripts.js
    cy.get('.network-put').click()

    cy.wait('@putComment')

    // our 404 statusCode logic in scripts.js executed
    cy.get('.network-put-comment').should('contain', message)
})

This is the Playwright for Python (pytest) version that I came up with:

def test_network_intercept(page: Page):
    page.goto("https://example.cypress.io/commands/network-requests")
    message = "whoa, this comment does not exist"

    def handle_route(route: Route):
        route.fulfill(
            status=404,
            body="{'error': '" + message + "'}",
            headers={"access-control-allow-origin": "*"},
        )
    page.route("**/comments/*", handle_route)
    page.locator(".network-put").click()
    expect(page.locator(".network-put-comment")).to_contain_text(message)

This test fails in the last line

expect(page.locator(".network-put-comment")).to_contain_text(message)

Can anybody tell me what is wrong with my code and how to fix it ?

Asked By: hpr

||

Answers:

You’re very close, but body="{'error': '" + message + "'}", looks iffy. JSON doesn’t allow single-quoted keys and values like that. Your response also doesn’t specify the content-type header as application/json.

The following passes, using json= rather than body=, letting Playwright encode the dictionary into JSON and set the appropriate headers:

from playwright.sync_api import expect, Page, Route, sync_playwright  # 1.37.0


def test_network_intercept(page: Page):
    page.goto("https://example.cypress.io/commands/network-requests")
    message = "whoa, this comment does not exist"

    def handle_route(route: Route):
        route.fulfill(
            status=404,
            json={"error": message},
            headers={"access-control-allow-origin": "*"},
        )

    page.route("**/comments/*", handle_route)
    page.locator(".network-put").click()
    expect(page.locator(".network-put-comment")).to_contain_text(message)


if __name__ == "__main__":
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        test_network_intercept(page)

For reference, if you wanted to do what json= does by hand, it’d look like:

import json

# ...
body=json.dumps({"error": message}),  # or hand-encode it, but with double quotes
headers={"content-type": "application/json"},  # add appropriate response type
# ...

To help debug your original code, you can run headfully or add logging before goto to see if there are errors in the console:

page.on("pageerror", lambda exc: print(f"uncaught exception: {exc}"))
page.goto() # ...

This gives uncaught exception: Cannot read properties of undefined (reading 'error'). Not the clearest error, but indicates that the response body isn’t coming through correctly.

Answered By: ggorlen