Metadata-Version: 2.1
Name: pytest-jscov
Version: 0.7.0
Summary: Pytest plugin for JavaScript coverage via Playwright CDP
License: MIT
Project-URL: Repository, https://github.com/HDembinski/pytest-jscov
Requires-Python: >=3.11
Requires-Dist: playwright>=1.58.0
Requires-Dist: pytest
Requires-Dist: pytest-cov
Provides-Extra: test
Requires-Dist: anyio>=4.0; extra == "test"
Description-Content-Type: text/markdown

# pytest-jscov

Get a single, unified coverage report for your full-stack Python + JS/TS application.

This pytest plugin collects JavaScript and TypeScript code coverage from
[Playwright](https://playwright.dev/python/) browser tests via Chrome's DevTools Protocol
(CDP) and merges it into [pytest-cov](https://github.com/pytest-dev/pytest-cov)'s combined
report. In other words, we use Chrome's profiler to track which lines of code were executed.

Using pytest-jscov is perfect if you write your frontend tests in Python with Playwright
already. Then you get coverage measurements on top with almost no extra effort.

## Features

- Collects V8 precise coverage from Chromium-based browsers via CDP
- Resolves inline sourcemaps (e.g. from esbuild) to map coverage back to
  original `.ts` source files
- Merges JS/TS line hits into pytest-cov so everything appears in one report
- Works with `--cov-branch`, but no branch coverage is collected
- Zero-config when coverage is not active: the `jscov` fixture is a no-op
  unless `--cov` is passed
- Full VS Code integration: coverage gutters work for JS/TS files just for Python

## Limitations

### Requirements for use

You must implement your tests in Python using the async Playwright API with the Chrome
browser for this to work. For details, see the installation section.

### Loss of coverage data due to page navigation

Coverage collection is not perfect when doing page nagivation. Coverage data is attached
to the currently executing page context. If that page reloads or navigates away before
`pytest-jscov` reads the coverage data, the old execution context is gone and its coverage
data with it.

We implement a partial workaround for this issue. When the plugin is active,
Playwright browser contexts created via `browser.new_context()` and pages
created via `browser.new_page()` are automatically instrumented so that
`reload`, `goto`, `go_back`, and `go_forward` flush coverage immediately
before those navigations run.

This improves coverage retention for navigations initiated through those page methods in
tests, but it does **not** catch page navigation triggered from JavaScript, such as

   `window.location.assign(...)`

For those cases, call `save_coverage(page)` just before the action that would
replace the page context:

```python
from pytest_jscov import save_coverage

await save_coverage(page)
await page.evaluate("window.location.assign('about:blank')")
```

### Detection of executable lines

We use a custom code to detect executable lines in JS/TS files to keep this project
lightweight. This code may not be perfect yet, if you encounter issues, drop an issue.

## Installation

```bash
pip install pytest-jscov
```

The plugin requires `pytest`, `pytest-cov`, and `playwright` (with Chromium installed).

### Register the coverage.py plugin

In your `pyproject.toml`:

```toml
[tool.coverage.run]
plugins = ["pytest_jscov.covplugin"]

[tool.coverage.pytest_jscov.covplugin]
static_root = "src/myapp/static"
```

`static_root` tells the plugin where your JS/TS source files live on disk, so
it can match coverage data to real files.

The plugin automatically switches `coverage.py` to the plugin-compatible tracing
`ctrace` core.

### Use normal Playwright page creation

```python
import pytest
from collections.abc import AsyncIterator
from playwright.async_api import Browser, Page

@pytest.fixture
async def page(browser: Browser) -> AsyncIterator[Page]:
   context = await browser.new_context()

   page = await context.new_page()
   await page.goto("http://localhost:8000")
   yield page

   await page.close()
   await context.close()
```

Any page created from that context will have its coverage recorded. The same
holds for pages created directly with `browser.new_page()`.

When the plugin is active, `browser.new_context()` and `browser.new_page()`
return instrumented objects so that:

- **On each `context.new_page()`:** opens a CDP session and starts V8 precise
   coverage for the new page
- **During the page lifetime:** flushes coverage before `page.reload()`,
   `page.goto()`, `page.go_back()`, `page.go_forward()`, and `page.close()`
- **During the page lifetime:** lets you call `await save_coverage(page)` to
   persist coverage before JS-triggered navigation
- **On `context.close()`:** collects coverage from all tracked pages, then
   detaches their CDP sessions

When `--cov` is not passed to pytest, Playwright is left alone.

### Run your tests

```bash
pytest --cov=src --cov-report=term
```

JS and TS files appear alongside Python in the coverage report:

```
Name                                  Stmts   Miss  Cover
----------------------------------------------------------
src/myapp/main.py                       180     90    50%
src/myapp/static/app.js                 441    144    67%
src/myapp/static/modules/foo.ts         194     14    93%
src/myapp/static/modules/bar.js         103     10    90%
----------------------------------------------------------
TOTAL                                   918    258    72%
```

### Filtering

Just like with Python sources, you can restrict the coverage report to individual files or directories by passing a path to `--cov`, for example:

   ```bash
   pytest --cov=src/myapp/static/foo.js
   ```

## How it works

1. The **pytest plugin** (`pytest_jscov.plugin`) provides the `jscov` fixture
   and a `pytest_runtestloop` hook. During each test, coverage entries from V8
   are accumulated in memory. After all tests complete, the accumulated data is
   written as a `.coverage.jscov` file that pytest-cov's `combine()` step picks
   up automatically.

2. The **coverage.py plugin** (`pytest_jscov.covplugin`) registers a file
   tracer and file reporter for JS/TS files. This teaches coverage.py how to
   find, read, and report on JavaScript and TypeScript source files.

### Sourcemap support

When a script contains an inline sourcemap
(`//# sourceMappingURL=data:application/json;base64,...`), the plugin decodes
the VLQ mappings and attributes coverage to the original source files. This
means if you use a bundler like esbuild to transpile TypeScript with
`--sourcemap=inline`, coverage is reported against your `.ts` files, not the
generated `.js`.

### IDE integration

When using the
[Python Testing](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
extension in VS Code, coverage gutters work for JS/TS files just like they do
for Python. VS Code runs pytest with `--cov --cov-branch` automatically, and
the merged report includes your frontend files — you'll see green/red line
markers directly in your `.ts` and `.js` sources.
