## TL;DR

The test tried to reach the app server and nothing was listening. Start the server before the tests in CI (webServer config or start-server-and-test), bind it to all interfaces if the browser runs in a separate container, and verify with curl before the test step.

## Error

```text
Error: page.goto: net::ERR_CONNECTION_REFUSED at YOUR_HOST/
```

## Steps

1. Add a `webServer` block to `playwright.config.ts` with the dev command, URL, and `reuseExistingServer: !process.env.CI`. Expected: Playwright starts the server and waits for the URL.
2. If the browser runs in a separate container, bind the server to `the all-interfaces address` and point tests at the service name, not YOUR_HOST. Expected: cross-container networking works.
3. Add a pre-test curl check in CI: `curl --retry 10 --retry-delay 2 YOUR_HOST/health`. Expected: the pipeline fails fast with a clear message instead of 50 test timeouts.
4. Check the port: CI services sometimes claim different ports than local. Print the actual listening port in the server logs. Expected: config port matches reality.
5. Re-run the CI job. Expected: goto succeeds because the server is provably up.

## When to use

- `ERR_CONNECTION_REFUSED` in CI, green locally.
- The app server is started by the pipeline, not already running.

## When not to use

- DNS failures (`ERR_NAME_NOT_RESOLVED`).
- The server is up but returns 500s (app bug).

## Tool compatibility

- Playwright 1.30 through latest; `webServer` config; any CI provider.

## Variant phrasings

### Playwright could not connect to YOUR_HOST:3000 in CI

Same root cause; the server lifecycle is the fix.

### webServer timeout waiting for URL

The server took too long to boot; raise the webServer timeout, not the test timeout.

## Why it happens

Locally the dev server is already running from your terminal. In CI nothing is running unless the pipeline starts it, and tests race the boot.

## Edge cases

- `reuseExistingServer: true` in CI hides the problem locally but breaks CI; gate it on `!process.env.CI`.
- Databases also need to be up; a refusing DB looks like an app failure one layer down.
- IPv6 YOUR_HOST (`the loopback address`) vs IPv4 (`the loopback address`) mismatches cause refuses that curl with the other family would catch.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_5yQTGW_-kae-ZRamllnhcQ
