Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,27 @@ else
FixPath = $1
endif

E2E_CONTAINER = e2e

serve: up $(SOURCE_DIR)/node_modules
@docker compose exec $(CONTAINER) sh -c "npm run dev"

test-e2e: up-e2e
@echo Installing JS dependencies in the e2e container if needed.
@docker compose exec $(E2E_CONTAINER) sh -c "[ -d node_modules/@playwright/test ] || npm install"
@echo Running Playwright end-to-end tests.
@docker compose exec $(E2E_CONTAINER) sh -c "npm run test:e2e"; status=$$?; \
printf '\nTo view the HTML report from Docker, run: make report-e2e\n'; \
printf '(Ignore the "npx playwright show-report" hint above -- that is the non-Docker command.)\n'; \
exit $$status

report-e2e: up-e2e
@echo Serving the Playwright HTML report at http://localhost:9323 -- press Ctrl-C to stop.
@docker compose exec $(E2E_CONTAINER) sh -c "npx playwright show-report --host 0.0.0.0 --port 9323"

up-e2e:
@docker compose --profile e2e up --detach $(E2E_CONTAINER)

up:
@docker compose up --detach

Expand Down Expand Up @@ -76,6 +94,6 @@ clean-js-modules:

clean: clean-js-dist clean-js-modules

.PHONY: serve up down build shell dist version \
.PHONY: serve test-e2e report-e2e up up-e2e down build shell dist version \
clean clean-astro-content clean-js-dist clean-js-modules \
.FORCE
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,66 @@ The next step is to determine which development environment you would like to us
* For instance, [`Makefile` support for VS Code](https://devblogs.microsoft.com/cppblog/now-announcing-makefile-support-in-visual-studio-code/)
3. Run `make serve` to launch the container, install the dependencies and run the development server.

#### Running the end-to-end tests in Docker

The Playwright tests run in a dedicated container (the official Playwright image, with the browsers and their system libraries baked in). It is gated behind the `e2e` Compose profile, so it is not built or started by `make serve` — you only pay for it when you run the tests. You do **not** need the site running (locally or via `make serve`) first; the tests start their own dev server inside the container.

1. Run `make test-e2e`. This builds/starts the `e2e` container, installs its dependencies the first time, and runs the Playwright suite.
2. Run `make report-e2e` to view the HTML report from the last run. It is served from the container at <http://localhost:9323> (press Ctrl-C to stop) — no local Playwright install required.

The e2e container keeps its own `node_modules` in a Docker volume rather than sharing the host's. This is required because Rollup/esbuild ship per-OS native binaries: a macOS host `node_modules` cannot run in the Linux container. It also means running the tests will not disturb the `node_modules` you use for local development.

### Setting up a Development Environment using Node.

1. Install the [LTS version of Node](https://nodejs.org/en/download/prebuilt-installer/current) on your development machine.
2. Run `npm install` from the `site` directory to install the JS dependencies.
3. Run `npm run dev` from the `site` directory to run the development server.
3. Run `npm run dev` from the `site` directory to run the development server.

#### Running the end-to-end tests with Node

The Playwright tests need a browser. Without Docker, install it once with:

```sh
npm run playwright:check
```

This runs `playwright install --with-deps`, which downloads the browser binaries. The `--with-deps` flag also installs the required system libraries on Linux (it is a no-op on macOS/Windows and may prompt for `sudo` on Linux). Then run the suite with:

```sh
npm run test:e2e # run the tests headless
npm run test:e2e:ui # run with the Playwright UI
npm run test:e2e:report # open the HTML report from the last run
```

## Troubleshooting

### Switching between `make serve` (Docker) and `npm run dev` (local) breaks the build

If you run the app **both** ways, you may hit an error like:

```
Error: Cannot find module @rollup/rollup-linux-arm64-gnu
```

(or the `darwin`/`win32` equivalent, or `sh: 1: astro: not found`).

**Why:** `make serve` runs the app in a Linux container, while `npm run dev` runs it
directly on your machine (e.g. macOS). Both share the **same** `astro/node_modules`
folder, but Rollup and esbuild install **per-OS native binaries**. Whichever
environment installed dependencies last wins, so the other one can no longer find the
binary built for its platform.

**Fix:** reinstall dependencies for the environment you are switching *to* — you do
not need to (and on macOS often cannot) delete `node_modules` first; reinstalling
overwrites the platform-specific packages in place.

- Switching to **local** (`npm run dev`): run `npm install` from the `astro` directory.
- Switching to **Docker** (`make serve`): reinstall inside the container, e.g.
`docker compose exec website sh -c "npm install"`.

If you really want to clear `node_modules` and Docker Desktop reports "Permission
denied" deleting it from macOS, remove its contents from inside the container instead:
`docker compose exec website sh -c "rm -rf node_modules/*"`, then reinstall.

> The Playwright e2e container is **not** affected — it keeps its own `node_modules`
> in a Docker volume, so `make test-e2e` works regardless of what is installed locally.
5 changes: 5 additions & 0 deletions astro/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ pnpm-debug.log*
.vscode
# Local Netlify folder
.netlify

# Playwright
test-results/
playwright-report/
playwright/.cache/
13 changes: 13 additions & 0 deletions astro/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,16 @@ WORKDIR /website/astro

EXPOSE 4321
ENTRYPOINT ["tail", "-f", "/dev/null"]

###############################
# End-to-end test image. Uses the official Playwright image so the browsers and
# their system libraries are baked in and version-matched to @playwright/test.
# Keep this tag in sync with the resolved @playwright/test version in
# astro/package-lock.json (currently 1.61.1). If a test run reports a version
# mismatch, bump this tag to match and rebuild (`make build`).
FROM mcr.microsoft.com/playwright:v1.61.1-noble AS e2e

WORKDIR /website/astro

EXPOSE 4321
ENTRYPOINT ["tail", "-f", "/dev/null"]
88 changes: 88 additions & 0 deletions astro/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"astro:check": "astro check --tsconfig tsconfig.json"
"astro:check": "astro check --tsconfig tsconfig.json",
"playwright:check": "npx playwright install --with-deps",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:report": "npx playwright show-report"
},
"dependencies": {
"@astrojs/mdx": "^5.0.6",
Expand All @@ -34,6 +38,8 @@
},
"devDependencies": {
"@astrojs/check": "^0.9.9",
"@axe-core/playwright": "^4.11.3",
"@playwright/test": "^1.61.1",
"@types/lodash-es": "^4.17.12",
"prettier": "^3.2.5",
"prettier-plugin-astro": "^0.14.0"
Expand Down
29 changes: 29 additions & 0 deletions astro/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
// In the Docker e2e container, never auto-open/serve the report after a run:
// it would bind to the container's localhost (unreachable via the port map)
// and block the run. View it explicitly there with `make report-e2e`. Local
// (non-Docker) runs keep the convenient auto-open on failure.
reporter: [["html", { open: process.env.DOCKER_E2E ? "never" : "on-failure" }]],
use: {
baseURL: "http://localhost:4321",
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: {
command: "npm run dev",
url: "http://localhost:4321",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
51 changes: 51 additions & 0 deletions astro/tests/e2e/blog-authors.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";

test.describe("Blog authors index (/blog/authors/)", () => {
test("renders author names as strings, not [object Object]", async ({ page }) => {
await page.goto("/blog/authors/");

await expect(page.locator("body")).not.toContainText("[object Object]");
await expect(
page.getByRole("link", { name: /Rachael Bradley Montgomery/i }),
).toBeVisible();
});

test("axe accessibility scan (informational, not gating)", async ({ page }) => {
await page.goto("/blog/authors/");
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
});

test.describe("Author detail page (/blog/authors/rachael-bradley-montgomery/)", () => {
const path = "/blog/authors/rachael-bradley-montgomery/";

test("header shows formatted author name, not [object Object]", async ({ page }) => {
await page.goto(path);

await expect(page.locator("body")).not.toContainText("[object Object]");
await expect(page.locator("body")).toContainText(
"Blogs by Rachael Bradley Montgomery",
);
});

test("breadcrumbs show ancestors and formatted author name as current page", async ({ page }) => {
await page.goto(path);

const nav = page.getByRole("navigation", { name: "Breadcrumb" });
await expect(nav).toBeVisible();

await expect(nav.getByRole("link")).toHaveText(["Home", "Blog", "Authors"]);

const current = nav.locator('[aria-current="page"]');
await expect(current).toHaveText("Blogs by Rachael Bradley Montgomery");
await expect(current).not.toContainText("[object Object]");
});

test("axe accessibility scan (informational, not gating)", async ({ page }) => {
await page.goto(path);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
});
27 changes: 27 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,30 @@ services:
- 4322:4322
volumes:
- '.:/website'

# End-to-end (Playwright) tests. Gated behind the `e2e` profile so it is not
# built or started by a plain `docker compose up` / `make serve`. The tests
# start their own dev server inside this container.
e2e:
container_name: website-e2e
profiles:
- e2e
build:
context: ./astro
target: e2e
# Tells playwright.config.ts not to auto-open/serve the report after a run
# (it would bind container-localhost and block); use `make report-e2e`.
environment:
- DOCKER_E2E=1
# 9323 is the port `playwright show-report` serves on (see `make report-e2e`).
ports:
- 9323:9323
volumes:
- '.:/website'
# Isolate node_modules in a container-private volume so the host's macOS
# binaries don't leak in (rollup/esbuild ship per-OS native modules). The
# e2e container installs its own Linux deps here via `make test-e2e`.
- 'e2e-node-modules:/website/astro/node_modules'

volumes:
e2e-node-modules:
Loading