Repository: microsoft/playwright-python

Issue:
[BUG] Now work mocking with route.fulfill json
**Context:**
-  Playwright Version: [Version 1.29]
- Operating System: [Windows, Mac]
- Python version: [3.10]
- Browser: [Chromium,]


**Code Snippet**

```
def test_mock_tags(page):

    def handle_route(route: Route):

        response = route.fetch()
        json = response.json()
        json["tags"] = ["implementations", "deserunt"]
        route.fulfill(json=json, response=response)


    page.route("**/api/tags", handle_route)
    page.goto('https://demo.realworld.io/')
```


**Describe the bug**
There is no mocking of json data.




Discussion:
Thanks for the report! I was able to reproduce and find unexpected behavior:

```
from playwright.sync_api import Page, Route, expect

def test_unmodified(page: Page) -> None:
    page.goto("https://demo.realworld.io/")
    sidebar = page.locator('css=div.sidebar') 
    expect(sidebar.get_by_role('link')).to_contain_text(['implementations', 'welcome', 'introduction', 'codebaseShow', 'ipsum', 'qui', 'quia', 'cupiditate', 'et', 'deserunt'])

def test_intercepted(page: Page) -> None:
    def handle_route(route: Route):
        response = route.fetch()
        json = response.json()
        print("!!!!!!!!!")
        print(json)
        json["tags"] = ["implementations", "deserunt"]
        route.fulfill(json=json, response=response)

    page.route("**/api/tags", handle_route)

    page.goto("https://demo.realworld.io/")
    sidebar = page.locator('css=div.sidebar') 
    expect(sidebar.get_by_role('link')).to_contain_text(["implementations", "deserunt"])
```

The first test passes, the second fails with:

```
  File "/Users/rosswollman/Desktop/1693/env/lib/python3.10/site-packages/playwright/_impl/_network.py", line 302, in fulfill
    body = json.dumps(json)
AttributeError: 'dict' object has no attribute 'dumps'
```

Looks like a name shadow in the code. Will investigate and add appropriate tests.
PR opened with fix. This should get cherry-picked into a 1.29.X patch release.

Resolving patch:
diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py
--- a/playwright/_impl/_network.py
+++ b/playwright/_impl/_network.py
@@ -16,6 +16,7 @@
 import base64
 import inspect
 import json
+import json as json_utils
 import mimetypes
 import sys
 from collections import defaultdict
@@ -288,6 +289,12 @@ async def fulfill(
     ) -> None:
         self._check_not_handled()
         params = locals_to_params(locals())
+
+        if json is not None:
+            if body is not None:
+                raise Error("Can specify either body or json parameters")
+            body = json_utils.dumps(json)
+
         if response:
             del params["response"]
             params["status"] = (
@@ -298,8 +305,6 @@ async def fulfill(
             )
             from playwright._impl._fetch import APIResponse
 
-            if json is not None:
-                body = json.dumps(json)
             if body is None and path is None and isinstance(response, APIResponse):
                 if response._request._connection is self._connection:
                     params["fetchResponseUid"] = response._fetch_uid


Test patch:
diff --git a/tests/async/test_request_fulfill.py b/tests/async/test_request_fulfill.py
--- a/tests/async/test_request_fulfill.py
+++ b/tests/async/test_request_fulfill.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import json
+
 from playwright.async_api import Page, Route
 from tests.server import Server
 
@@ -25,3 +27,52 @@ async def handle(route: Route):
     response = await page.goto(server.PREFIX + "/title.html")
     assert response.status == 200
     assert await page.title() == "Woof-Woof"
+
+
+async def test_should_fulfill_json(page: Page, server: Server) -> None:
+    async def handle(route: Route) -> None:
+        await route.fulfill(status=201, headers={"foo": "bar"}, json={"bar": "baz"})
+
+    await page.route("**/*", handle)
+
+    response = await page.goto(server.EMPTY_PAGE)
+    assert response
+    assert response.status == 201
+    assert response.headers["content-type"] == "application/json"
+    assert json.loads(await page.evaluate("document.body.textContent")) == {
+        "bar": "baz"
+    }
+
+
+async def test_should_fulfill_json_overriding_existing_response(
+    page: Page, server: Server
+) -> None:
+    server.set_route(
+        "/tags",
+        lambda request: (
+            request.setHeader("foo", "bar"),
+            request.write('{"tags": ["a", "b"]}'.encode()),
+            request.finish(),
+        ),
+    )
+
+    original = {}
+
+    async def handle(route: Route) -> None:
+        response = await route.fetch()
+        json = await response.json()
+        original["tags"] = json["tags"]
+        json["tags"] = ["c"]
+        await route.fulfill(response=response, json=json)
+
+    await page.route("**/*", handle)
+
+    response = await page.goto(server.PREFIX + "/tags")
+    assert response
+    assert response.status == 200
+    assert response.headers["content-type"] == "application/json"
+    assert response.headers["foo"] == "bar"
+    assert original["tags"] == ["a", "b"]
+    assert json.loads(await page.evaluate("document.body.textContent")) == {
+        "tags": ["c"]
+    }
diff --git a/tests/sync/test_request_fulfill.py b/tests/sync/test_request_fulfill.py
--- a/tests/sync/test_request_fulfill.py
+++ b/tests/sync/test_request_fulfill.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import json
+
 from playwright.sync_api import Page, Route
 from tests.server import Server
 
@@ -26,3 +28,48 @@ def handle(route: Route) -> None:
     assert response
     assert response.status == 200
     assert page.title() == "Woof-Woof"
+
+
+def test_should_fulfill_json(page: Page, server: Server) -> None:
+    def handle(route: Route) -> None:
+        route.fulfill(status=201, headers={"foo": "bar"}, json={"bar": "baz"})
+
+    page.route("**/*", handle)
+
+    response = page.goto(server.EMPTY_PAGE)
+    assert response
+    assert response.status == 201
+    assert response.headers["content-type"] == "application/json"
+    assert json.loads(page.evaluate("document.body.textContent")) == {"bar": "baz"}
+
+
+def test_should_fulfill_json_overriding_existing_response(
+    page: Page, server: Server
+) -> None:
+    server.set_route(
+        "/tags",
+        lambda request: (
+            request.setHeader("foo", "bar"),
+            request.write('{"tags": ["a", "b"]}'.encode()),
+            request.finish(),
+        ),
+    )
+
+    original = {}
+
+    def handle(route: Route) -> None:
+        response = route.fetch()
+        json = response.json()
+        original["tags"] = json["tags"]
+        json["tags"] = ["c"]
+        route.fulfill(response=response, json=json)
+
+    page.route("**/*", handle)
+
+    response = page.goto(server.PREFIX + "/tags")
+    assert response
+    assert response.status == 200
+    assert response.headers["content-type"] == "application/json"
+    assert response.headers["foo"] == "bar"
+    assert original["tags"] == ["a", "b"]
+    assert json.loads(page.evaluate("document.body.textContent")) == {"tags": ["c"]}