|
| 1 | +# Playwright Interceptors |
| 2 | + |
| 3 | +PyScript testing needs a real browser -- web components which load Pyodide and execute Python, then change the DOM. |
| 4 | +[Playwright](https://playwright.dev/python/) provides this, but we'd like more convenience in the testing: |
| 5 | + |
| 6 | +- Don't actually launch a web server to fetch the examples |
| 7 | +- Make it easier to write examples and tests by having some automation |
| 8 | + |
| 9 | +In this step we bring in Playwright, but don't yet use PyScript. |
| 10 | +Here's the big idea: we do _not_ run a web server. |
| 11 | +Instead, we write a Playwright interceptor as a pytest fixture. |
| 12 | + |
| 13 | +## Install Playwright |
| 14 | + |
| 15 | +We need to add Playwright to PSC. |
| 16 | +In the current repo, this is done as part of a Makefile rule, which also copies the examples to a relative directory (bleh). |
| 17 | + |
| 18 | +Instead, we'll just make it a development dependency. |
| 19 | +If a Contributor wants to write an example, they just need to clone the repo and do a Poetry install with dev dependencies. |
| 20 | +Kind of normal Python dev workflow. |
| 21 | +To make it work in CI using Nox, we added this dependency to the `noxfile.py`. |
| 22 | + |
| 23 | +This still requires running `playwright install` manually, to get the Playwright browsers globally installed. |
| 24 | +That has to be added to PSC Contributor documentation. |
| 25 | + |
| 26 | +## Fixture |
| 27 | + |
| 28 | +With Playwright installed, now it is time to make it easier to write/run the tests for examples. |
| 29 | +In the previous step, we did "shallow" testing of an example, using `TestClient` to ensure the HTML was returned. |
| 30 | +We didn't actually load the HTML into a DOM, certainly didn't evaluate the PyScript web components, and _definitely_ didn't run some Python in Pyodide. |
| 31 | + |
| 32 | +The current Collective uses Playwright's `page` fixture directly: you provide a URL, it tells the browser to make an HTTP request. |
| 33 | +This means it needs an HTTP server running. |
| 34 | +The repo fires up and shuts down a Python `SimpleHTTPServer` running in a thread, as part of test running. |
| 35 | + |
| 36 | +If something gets hung...ouch. |
| 37 | +You have to wait for the thread to time out. |
| 38 | + |
| 39 | +PSC changes this by not running an HTTP server for testing the examples. |
| 40 | +Instead, we use [Playwright interceptors](https://playwright.dev/python/docs/network#modify-responses). |
| 41 | +When the URL comes in, our Python code runs and returns a response...quite like `TestClient` pretends to run an ASGI server. |
| 42 | +Our "interceptor" looks at the URL, and if it is to the "fake" server, it reads/returns the path from disk. |
| 43 | + |
| 44 | +This fixture is software, so we'll make a file at `src/psc/fixtures.py` and a test at `test_fixtures.py`. |
| 45 | +We also need to make `tests/conftest.py` to load this as a pytest plugin. |
| 46 | +The test file has dummy objects for the Playwright request/response/page/route etc. |
| 47 | +The tests exercise the main code paths we need for the interceptor: |
| 48 | + |
| 49 | +- A request but _not_ to the fake server URL should just be passed-through to an HTTP request |
| 50 | +- A request to the fake server URL should extract the path |
| 51 | + - If that path exists in the project, read the file and return it |
| 52 | + - If not, raise a value error |
| 53 | + |
| 54 | +With that in place, we write a `fixtures.fake_page` fixture function. |
| 55 | +It asks `pytest` to inject the real `page`. |
| 56 | +It then installs the interceptor by calling a helper function. |
| 57 | +This helper function is what we actually write the fixture test for. |
| 58 | + |
| 59 | +## Serve Up Examples |
| 60 | + |
| 61 | +We aren't going to test by fetching examples from an HTTP server. |
| 62 | +But our Viewers will look at examples from the HTTP server we made in the previous step. |
| 63 | +Let's add that to `app.py` with another `Mount`, this time pointing `/examples` at `src/psc/examples`. |
| 64 | +Also, add `first.html` with some dummy text as an "example". |
| 65 | + |
| 66 | +Before the implementation, we add `test_app.test_first_example` as a failing test. |
| 67 | +Then, once `app.py` is fixed, the test will pass. |
| 68 | + |
| 69 | +## First Test |
| 70 | + |
| 71 | +Our fixture is now in place, with a test that has good coverage. |
| 72 | +We have a dummy example in `first.html`. |
| 73 | +Let's write a test that uses Playwright and the interceptor. |
| 74 | + |
| 75 | +We just added a `TestClient` test -- a kind of "shallow" test -- for `first.html`. |
| 76 | +In `test_app.py` we add `test_first_example_full` as a Playwright test. |
| 77 | + |
| 78 | +When we first run it, we see `fixture 'fake_page' not found`. |
| 79 | +This is because `conftest.py` needs to load the `psc.fixtures`. |
| 80 | +With that line added, the tests pass. |
| 81 | + |
| 82 | +## Shallow vs. Full Markers |
| 83 | + |
| 84 | +These Playwright tests are SLOW. |
| 85 | +When we get a bunch of examples, it's going to be a pain. |
| 86 | +As such, we'll want to emphasize unit tests and the shallow `TestClient` tests. |
| 87 | + |
| 88 | +To make this first-class, we'll add 3 pytest markers to the project: unit, shallow, and full. |
| 89 | +We do so in `pyproject.toml` along with the option to warn if someone uses an undefined customer marker. |
| 90 | + |
| 91 | +With this in place, we add decorators such as `@pytest.mark.full` to our tests. |
| 92 | +Later, we can run `pytest -m "not full"` to skip the Playwright tests. |
| 93 | + |
| 94 | +## Better `TestClient` Testing |
| 95 | + |
| 96 | +In this step we also improved the `TestClient` tests which makes sure our Starlette app serves what we expect. |
| 97 | +Part of that means testing HTML, so we installed `beatifulsoup4` and made a fixture that lets us issue a request and get back `BeautifulSoup`. |
| 98 | +These have rich CSS selector locators, so very convenient to use in tests. |
| 99 | +This fixture also raise an exception if it doesn't get a `200` so you don't have to test that any more. |
| 100 | + |
| 101 | +Just to be clear: `TestClient` tests are nice when you do *not* need to get into the JS/Pyodide side. |
| 102 | +They are fast and zero mystery. |
| 103 | + |
| 104 | +Of course, we added tests for those fixtures. |
| 105 | + |
| 106 | +## QA |
| 107 | + |
| 108 | +Cleaned up everything for pre-commit, mypy, nox, etc. |
| 109 | +Coverage is still 100%. |
| 110 | + |
| 111 | +Along the way, Typeguard got mad at the introduction of the marker. |
| 112 | +I skipped investigation and just disabled Typeguard from the noxfile for now. |
0 commit comments