Skip to content

Add integration testing with testcontainers and Playwright#442

Open
prk-Jr wants to merge 19 commits intomainfrom
feature/integration-testing-with-testcontainers
Open

Add integration testing with testcontainers and Playwright#442
prk-Jr wants to merge 19 commits intomainfrom
feature/integration-testing-with-testcontainers

Conversation

@prk-Jr
Copy link
Collaborator

@prk-Jr prk-Jr commented Mar 5, 2026

Summary

  • Add end-to-end integration testing infrastructure using testcontainers for HTTP-level tests and Playwright for browser tests against WordPress and Next.js containers
  • Add Playwright browser tests verifying script injection, SPA navigation, deferred script execution, API passthrough, and form URL rewriting in a real Chromium browser
  • Introduce a /health endpoint and CI workflow for running both test suites separately from the main CI

Changes

File Change
crates/fastly/src/main.rs Add /health GET endpoint returning 200 with "ok" body
crates/integration-tests/Cargo.toml New crate with reqwest, testcontainers, and tokio dependencies
crates/integration-tests/tests/common/ Core test infrastructure: runtime traits, config builder, assertion helpers including assert_form_action_rewritten (9 unit tests)
crates/integration-tests/tests/environments/ Fastly/Viceroy runtime environment with dynamic port allocation
crates/integration-tests/tests/frameworks/ Framework abstractions for WordPress and Next.js with 7 standard + 5 custom scenarios
crates/integration-tests/tests/integration.rs Matrix test runner (test_all_combinations, per-framework tests)
crates/integration-tests/fixtures/configs/ Viceroy and Fastly config templates with KV stores and secrets
crates/integration-tests/fixtures/frameworks/wordpress/ WordPress Docker image: PHP built-in server with minimal test theme and /wp-admin/ stub
crates/integration-tests/fixtures/frameworks/nextjs/ Next.js 14 Docker image: 4 pages, API routes, forms, shared navigation with hydration signal, deferred route scripts
crates/integration-tests/browser/ Playwright test suite: global setup/teardown, Docker + Viceroy infra helpers with cleanup-on-failure
crates/integration-tests/browser/tests/shared/ Cross-framework browser tests: script injection, script bundle loading
crates/integration-tests/browser/tests/nextjs/ Next.js browser tests: 4-page SPA navigation chain, deferred script execution, API passthrough, form URL rewriting
crates/integration-tests/browser/tests/wordpress/ WordPress browser tests: admin page script injection
.github/workflows/integration-tests.yml CI workflow with parallel HTTP-level and browser test jobs, split artifact uploads for Playwright reports and traces
scripts/integration-tests.sh Shell script for HTTP-level test orchestration (WASM build + Docker + test run)
scripts/integration-tests-browser.sh Shell script for browser test orchestration (WASM build + Docker + Playwright install + test run)
crates/integration-tests/README.md Full documentation: prerequisites, quick start, test scenario tables, architecture overview
INTEGRATION_TESTS_PLAN.md Design document with architecture decisions and progress tracking
Cargo.toml Add integration-tests to workspace members
Cargo.lock Updated lockfile with new dependencies

Test coverage

HTTP-level (Rust + testcontainers)

Scenario Frameworks
HtmlInjection WordPress, Next.js
ScriptServing WordPress, Next.js
AttributeRewriting WordPress, Next.js
ScriptServingUnknownFile404 WordPress, Next.js
WordPressAdminInjection WordPress
NextJsRscFlight Next.js
NextJsServerActions Next.js
NextJsApiRoute Next.js
NextJsFormAction Next.js

Browser-level (Playwright + Chromium)

Spec Frameworks
script-injection WordPress, Next.js
script-bundle WordPress, Next.js
navigation (4-page SPA chain + back button + deferred route script) Next.js
api-passthrough Next.js
form-rewriting Next.js
admin-injection WordPress

Totals: 12 Next.js + 5 WordPress browser tests, 0 flaky

Known gaps

  • GDPR consent propagation — the consent field exists in AuctionRequest but is not yet populated upstream. Requires implementation before it can be tested.
  • Browser suite uses manual Docker orchestration — HTTP-level tests use the Rust testcontainers crate; browser tests use docker run/docker stop via Playwright's globalSetup/globalTeardown (Node.js cannot use the Rust crate).

Closes

Closes #398

Test plan

  • cargo test --workspace (excluding integration-tests, which requires native target)
  • cargo clippy --all-targets --all-features -- -D warnings (excluding integration-tests)
  • cargo fmt --all -- --check
  • JS tests: pre-existing ESM/CJS failure on main (not introduced by this branch)
  • JS format: cd crates/js/lib && npm run format -- passes
  • Docs format: cd docs && npm run format -- passes
  • Integration-tests crate compiles for native target: cargo test -p integration-tests --target aarch64-apple-darwin --no-run
  • HTTP integration tests: ./scripts/integration-tests.sh -- all pass
  • Browser integration tests: ./scripts/integration-tests-browser.sh -- 17 pass, 0 fail
  • Cargo clippy on integration-tests: cargo clippy --manifest-path crates/integration-tests/Cargo.toml --tests -- clean

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses log macros (not println!)
  • New code has tests (9 unit tests for assertions and config, 17 browser tests)
  • No secrets or credentials committed

@prk-Jr prk-Jr self-assigned this Mar 5, 2026
@prk-Jr prk-Jr marked this pull request as draft March 5, 2026 14:12
prk-Jr added 7 commits March 5, 2026 20:12
The integration-tests crate defines traits, error types, and helpers
consumed by Docker-dependent tests. Without Docker the compiler sees
them as unused and -D warnings promotes them to errors. A crate-level
allow keeps CI green.
Proves that a next/script with strategy="afterInteractive" executes
after a client-side navigation through the trusted-server proxy.
The test SPA-navigates from / to /dashboard, waits for the script's
global marker, asserts it ran exactly once, and confirms no
document-level request fired (true SPA, not full reload).
@prk-Jr prk-Jr changed the title Add integration testing infrastructure with testcontainers Add integration testing with testcontainers and Playwright Mar 7, 2026
@prk-Jr prk-Jr marked this pull request as ready for review March 9, 2026 14:47
Copy link
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid integration testing infrastructure. The RuntimeEnvironment + FrontendFramework matrix design is clean and extensible, the two-layer approach (Rust HTTP + Playwright browser) gives good coverage, and the assertion library having its own unit tests is a nice touch.

Cross-cutting notes

📝 Separate Cargo.lock — The 3400-line crates/integration-tests/Cargo.lock is disconnected from the workspace lock. If error-stack (or any shared dep) is updated in the workspace but not here, they silently diverge. Consider documenting the upgrade process or adding a CI check that validates shared dependency versions stay in sync.

📝 Viceroy config placeholdersviceroy-template.toml uses key = "placeholder" / data = "placeholder" for all KV stores. This means integration tests don't exercise KV store functionality, which is fine but worth a comment in the config noting this intentional scope limitation.

.gitignore missing trailing newline — The last line lacks a POSIX-compliant trailing newline.

What's done well

👍 Drop impl on ViceroyHandle ensures process cleanup even on panics — good defensive design.

👍 Assertion functions (assertions.rs) have thorough unit tests covering both passing and failing cases.

👍 Browser tests use hydration detection (data-hydrated attribute) before SPA navigation — avoids the classic "clicked before client router was ready" flake.

👍 Health check endpoint is namespaced (/__trusted-server/health) to minimize publisher route conflicts.

👍 Shell scripts detect native target dynamically via rustc -vV — no hardcoded platform assumptions.

👍 Smart use of PHP built-in server for WordPress fixture — avoids MySQL dependency while still testing real HTML processing behavior.

branches: [main]
pull_request:
pull_request_review:
types: [submitted]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌱 pull_request_review trigger means integration tests re-run on every review submission, even when no code changed. Combined with the pull_request trigger, an approval on an existing PR triggers a full redundant run. Consider removing this trigger or using workflow_dispatch for on-demand runs.

Copy link
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below

Copy link
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One follow-up I still recommend:

.github/workflows/integration-tests.yml currently runs on pull_request_review approvals, which can trigger full integration + browser reruns without any code change.
Severity: P2 (efficiency/cost)
Suggestion: remove the pull_request_review trigger and keep pull_request + workflow_dispatch for canonical/manual runs.
Verdict: needs discussion (not a hard blocker).

Comment on lines +10 to +11
pull_request_review:
types: [submitted]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 (efficiency/cost): pull_request_review triggers will re-run the full integration + browser test suites on every review submission — even when there are no code changes. Consider removing this trigger and keeping just pull_request + workflow_dispatch for canonical/manual runs.

Copy link
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

All 12 CI checks pass. The architecture is solid — clean separation between runtime environments, frontend frameworks, and test scenarios; proper error-stack usage; good Drop-based cleanup; thoughtful extensibility patterns.

Three blocking issues need attention before merge, plus six non-blocking suggestions.

Blocking: 3 · Non-blocking: 6

- name: Install Viceroy
if: steps.cache-viceroy.outputs.cache-hit != 'true'
shell: bash
run: cargo install --git https://github.com/fastly/Viceroy viceroy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Blocking — Unpinned Viceroy install

Both the cache key (line 41, git ls-remote HEAD) and this install use whatever is latest on Viceroy's default branch. A breaking upstream change will break CI with zero code changes in this repo.

Pin to a specific tag or --rev:

run: cargo install --git https://github.com/fastly/Viceroy --tag viceroy-v0.12.1 viceroy

And update the cache key to match the pinned rev instead of HEAD.


on:
push:
branches: [main]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Blocking — Workflow still triggers on push: main

Commit 5b00113 says "Trigger integration test on pull requests only" but this trigger is still here. Every merged PR runs the full Docker+WASM+Playwright suite twice (on the PR and on the push to main).

Remove the push trigger or make the duplication intentional with a comment explaining why.

jobs:
integration-tests:
name: integration tests
runs-on: ubuntu-latest
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Blocking — No timeout-minutes on CI jobs

Neither integration-tests nor browser-tests has a timeout-minutes. If Viceroy hangs or a container stalls, jobs run until GitHub's default 6-hour timeout, silently burning CI minutes.

jobs:
  integration-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 15


// If the response is a Flight payload, it must not contain injected scripts
if body.contains("/static/tsjs=") {
return Err(Report::new(TestError::ScriptTagNotFound)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Non-blocking — Wrong error variant semantics

TestError::ScriptTagNotFound fires when a script tag was found but shouldn't have been (RSC Flight response). The variant name says the opposite of the actual condition. A variant like UnexpectedScriptInjection would be semantically correct and avoid confusing future readers of error output.

uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Non-blocking — Playwright report overwritten between framework runs

The Next.js run (line 68) writes to playwright-report/, then the WordPress run (line 77) overwrites it. This artifact upload only captures the WordPress report. If Next.js fails but WordPress passes, the failure report is lost.

Consider distinct artifact names per framework, or separate report output directories via PLAYWRIGHT_HTML_REPORT env var.


test.beforeEach(async ({}, testInfo) => {
const state = readState();
if (state.framework !== "nextjs") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Non-blocking — Silent full-skip on framework typo

This pattern appears in 4+ spec files. If TEST_FRAMEWORK has a typo (e.g. nexjs), all gated tests silently skip → green CI with zero assertions run. Consider a guard in readState() or the beforeEach that fails on unrecognized framework values.

// These modules are shared by ignored Docker-backed tests and unit tests
// inside the same integration test target, so some items are intentionally
// unused in a plain `cargo test` compile.
#[allow(dead_code, unused_imports)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Non-blocking — Blanket lint suppression

This suppresses all dead_code and unused_imports warnings across three entire modules. It could mask real dead code that accumulates over time. Consider targeting #[allow] to specific items that are only used in the #[ignore]d tests.

/** Kill a Viceroy process. */
export function stopViceroy(pid: number): void {
try {
process.kill(pid, "SIGTERM");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌱 Non-blocking — stopViceroy sends SIGTERM without awaiting exit

process.kill(pid, "SIGTERM") returns immediately. If the suite runs frameworks back-to-back, the old Viceroy process could still hold its port when the next one tries to start. Consider awaiting the process exit or adding a short grace period.

run: >-
cargo test
--manifest-path crates/integration-tests/Cargo.toml
--target x86_64-unknown-linux-gnu
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📌 Non-blocking — Hardcoded target triple

x86_64-unknown-linux-gnu is hardcoded here, while the local script (integration-tests.sh) auto-detects via rustc -vV. Fine today on ubuntu-latest, but inconsistent with the local script approach. Consider aligning them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integration tests: WordPress and Next.js frontends with Testcontainers

3 participants