# Write Playwright Tests for a Symfony App

Yes. An agent writes Playwright specs for your Symfony app's sign-in, browsing and comment flows, runs them against the PHP server in its sandbox, and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/write-playwright-tests/symfony/), in a Project that has your repository.

## What the agent does

The agent reads how the app runs and what it already tests. In a Symfony app like `symfony/demo`, `composer.json` requires PHP 8.4, Symfony 8, Doctrine ORM 3 and `ext-pdo_sqlite`; `.env` points `DATABASE_URL` at `data/database.sqlite`, and `data/` ships that file beside a `database_test.sqlite`. There is no `package.json`: assets go through AssetMapper and `importmap.php`, with Stimulus controllers under `assets/controllers`. `phpunit.dist.xml` runs the suite under `tests/` with the DAMA transaction bundle, and `tests/Controller/BlogControllerTest.php` already covers the blog index, the RSS feed, a new comment and search through `WebTestCase`, which drives the kernel without a browser. The README starts the app with `symfony serve` or `php -S localhost:8000 -t public/`, and `.github/workflows/tests.yaml` installs PHP 8.4 and 8.5 with `intl`, `mbstring`, `pdo_sqlite` and `zip` and runs `vendor/bin/phpunit`.

The flows come from the routes and fixtures. `src/Controller/BlogController.php` serves `/{_locale}/blog/`, paginated pages with ten posts each from `Paginator::PAGE_SIZE`, a post at `/blog/posts/{slug}`, comments at `POST /blog/comment/{postSlug}/new`, and `/blog/search`. `src/Controller/SecurityController.php` serves `/login`, whose template has `username`, `password` and `remember_me` inputs, and `src/Controller/UserController.php` guards `/profile/edit` and `/profile/change-password` with `ROLE_USER`. `src/DataFixtures/AppFixtures.php` creates `jane_admin` and `tom_admin` with `ROLE_ADMIN` and `john_user` with `ROLE_USER`, all with the password `kitten`, and gives each post five comments. Those are the accounts and the numbers the specs assert.

The agent adds a `tests/e2e/` directory with its own `package.json` for `@playwright/test`, so the PHP project gains no root Node dependency, and a `playwright.config.ts` whose `webServer` copies `data/database.sqlite` to a scratch path, exports `DATABASE_URL` for it, and starts `php -S 127.0.0.1:8000 -t public/` with `APP_ENV=test`. The specs: the blog index shows ten posts and the second page has different ones; a search for a word from the fixtures returns results; a visitor cannot open `/en/profile/edit` and is sent to `/en/login`; `john_user` signs in with `kitten`, opens a post, submits the `#post-add-comment` form and sees the comment appear; `jane_admin` reaches the admin blog controller and a plain user does not. Each spec uses the locale prefix the routes require, and the sign-in step is a shared fixture so it is written once.

It runs the suite headless in the sandbox, with the browser Playwright installs, until it passes, then runs `vendor/bin/phpunit` to show the PHP suite is unaffected. A `tests/e2e/README.md` gives the two commands to run locally, and a new job in `.github/workflows/tests.yaml` installs PHP the same way the existing job does, adds Node, installs the browser and runs the specs.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Write Playwright end-to-end tests for this Symfony app. Read composer.json, .env, phpunit.dist.xml, tests/Controller/BlogControllerTest.php, src/Controller/BlogController.php, src/Controller/SecurityController.php, src/Controller/UserController.php, src/DataFixtures/AppFixtures.php, the login template and .github/workflows/tests.yaml first. Put the tests in tests/e2e/ with their own package.json and a playwright.config.ts whose webServer copies data/database.sqlite to a scratch file, sets DATABASE_URL to it and starts php -S 127.0.0.1:8000 -t public/ with APP_ENV=test. Cover the paginated blog index, search, the redirect to login on a protected profile page, signing in as the fixture user and posting a comment, and the admin area being open to an admin and closed to a plain user. Share sign-in as a fixture. Run the specs headless until they pass, run vendor/bin/phpunit to show the PHP suite still passes, add a CI job for the specs, and open a pull request. If a spec exposes a real bug, mark it expected to fail and explain it in the description.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch with `tests/e2e/package.json`, `tests/e2e/package-lock.json`, `tests/e2e/playwright.config.ts`, the spec files and the sign-in fixture, a short README, and the new job in `.github/workflows/tests.yaml`. The description lists each flow, the fixture accounts it uses, and any spec marked as expected to fail with the reason.

The transcript holds the browser install, each Playwright run with its failures until the passing one, and the `vendor/bin/phpunit` run afterward, so a reviewer can see the specs were run against the PHP server rather than written blind. The repository's existing `Tests` workflow runs on the pull request as before, and the new job runs the specs beside it.

Before merging, a reviewer checks that the specs run against a copy of the SQLite file and never the committed one, that the comment spec cleans up or uses a copy so it stays repeatable, and that the selectors come from ids and labels in the templates rather than generated class names. A follow-up message to the same session adds the `/es/` locale or the password change flow, and the sandbox already has PHP and the browser installed.

## Run it on every Symfony major upgrade

Before an upgrade branch merges, start this run against it: a `POST /o/{org}/sessions` with the branch in `resources` as `https://github.com/<owner>/<repo>/tree/<branch>` and this prompt, so the flows a user sees are covered by the time the upgrade lands. An `Idempotency-Key` per branch makes a retried request return the run already started.

The same prompt runs in an automation with a `schedule` trigger to keep the suite in step with the templates, or from your own server with a `webhook_url` on the API key, which receives `run.completed` with the `session_url` for the pull request.

## Why run it on Rush?

Writing browser tests is a loop of running the server, running the spec, reading the failure and adjusting a selector, with a browser install and a PHP runtime to set up first. In a Rush sandbox the agent has PHP, the SQLite file, Node and a browser on its own machine, runs the loop as many times as it needs, and the sandbox parks when the turn ends.

The pull request carries the passing run and the PHPUnit run in its transcript, and the specs run on your repository's workflow from then on. The same prompt runs against several Symfony apps at once, each in its own sandbox, each ending in its own pull request with its own evidence.

## Frequently asked questions

### Why not use Symfony Panther instead of Playwright?

Panther is the PHP route and `phpunit.dist.xml` already has its extension commented out; it drives a browser through WebDriver from PHPUnit. The prompt asks for Playwright because that is what the team asked for; if you prefer Panther, say so in the input and the agent writes the same flows as Panther tests inside the PHPUnit suite instead.

### Does this add Node to my PHP project?

Only under `tests/e2e/`, which has its own `package.json`, so the root of the repository and AssetMapper are unchanged. CI installs Node for that job alone.

### Which database do the specs use?

A copy of `data/database.sqlite` made by the Playwright `webServer` command before the PHP server starts, with `DATABASE_URL` pointing at the copy. The committed file is never written to, and each run starts from the same fixtures.

### Will the existing PHPUnit tests change?

No. `tests/Controller` keeps covering the controllers through `WebTestCase`, and the agent runs `vendor/bin/phpunit` after the specs to show nothing there moved. The Playwright specs add the browser-level checks, such as the Stimulus login controller and the comment form, that kernel tests cannot see.
