From ef27ce69e17cb74bedb156e4d6b98c12ad15d188 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 19:22:46 +0000 Subject: [PATCH 1/3] fix(test): resolve BrowserTest base URL through layered lookup at instance time Specs running against a non-default port (Titan on 60050, scaffolds on 60080) previously had to compare getBaseUrl() against a sentinel string and override it manually because $resolveBaseUrl() returned http://localhost:8080 unconditionally and the only escape hatch (WHEELS_BROWSER_TEST_BASE_URL) is cached by the JVM at process start. $resolveBaseUrl() now consults, in order: this.baseUrl per-spec override, get("browserTestBaseUrl") Wheels setting, -Dwheels.browserTest.baseUrl JVM property, WHEELS_BROWSER_TEST_BASE_URL env, $detectBaseUrlFromCgi() auto-detect (the test runner reaches the suite over HTTP, so cgi already names the right host:port), then the localhost:8080 default. The CGI auto-detect skips when port==8080 so existing default-port runs are unchanged. Fixes #2779 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + .../BrowserTestBaseUrlResolutionSpec.cfc | 83 +++++++++++++++++++ vendor/wheels/wheelstest/BrowserTest.cfc | 70 +++++++++++++--- 3 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc diff --git a/CHANGELOG.md b/CHANGELOG.md index bc365070e2..512f0a1e72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo ### Fixed +- `BrowserTest`'s default base URL is no longer hardcoded to `http://localhost:8080`. `$resolveBaseUrl()` now consults a layered lookup at instance time: `this.baseUrl` (per-spec override) → `get("browserTestBaseUrl")` (Wheels setting) → `-Dwheels.browserTest.baseUrl=...` (JVM system property) → `WHEELS_BROWSER_TEST_BASE_URL` env var → `cgi.server_name`/`cgi.server_port` auto-detect → `http://localhost:8080` default. Specs running against a non-8080 server (Titan on 60050, `wheels new` scaffolds on 60080) can set `this.baseUrl` in `beforeAll()` or rely on the CGI auto-detect instead of comparing `getBaseUrl()` against a sentinel string. The bare-env-var approach still works for CI but is no longer the only escape hatch (the JVM caches env vars at process start, so post-launch `export` had no effect). Regression spec at `vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc` (#2779) - Linux `.deb` / `.rpm` packages double-nested the framework at `/opt/wheels/module/vendor/wheels/wheels/` instead of `/opt/wheels/module/vendor/wheels/`. `wheels-core-VER.zip` carries a top-level `wheels/` directory that `unzip` preserves; the nfpm `type: tree` rule then copied the entire `build/framework/` tree (wrapper and all) into the destination, leaving `Injector.cfc` one level too deep. Every fresh `wheels new` install on Ubuntu/Fedora then crashed on first request with `could not find component or class with name [wheels.Injector]`, cascading into the cryptic `The key [WO] does not exist.` error in `onError`. The brew formula handles this correctly via `(share/"wheels/framework/wheels").install Dir["*"]`; the Linux nfpm configs now pin `src` at `./build/framework/wheels/` to match. Regression spec at `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc` (#2773) - `onError` in the generated app template and demo `public/Application.cfc` now guards `application.wo` with `StructKeyExists(application, "wo")` after the recovery try/catch. When `new wheels.Injector(...)` fails during `onApplicationStart` (e.g. a stale `/wheels` mapping under Lucee Express 7), the original error is preserved via a minimal HTML fallback instead of cascading into the cryptic "The key [WO] does not exist" exception that hit "Your First 15 Minutes" tutorial users on fresh installs diff --git a/vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc b/vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc new file mode 100644 index 0000000000..1f354e0d4b --- /dev/null +++ b/vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc @@ -0,0 +1,83 @@ +/** + * Regression for #2779: BrowserTest's default base URL is hardcoded to + * http://localhost:8080 and the only escape hatch (the JVM-cached + * WHEELS_BROWSER_TEST_BASE_URL env var) cannot be changed after server + * start. Specs running against non-8080 servers must apply a manual + * fallback comparing getBaseUrl() to a sentinel string. + * + * After the fix, BrowserTest resolves the base URL through a layered + * lookup at instance time: + * 1. this.baseUrl (per-spec override) + * 2. get("browserTestBaseUrl") (Wheels setting) + * 3. JVM system property (wheels.browserTest.baseUrl) + * 4. Env var (WHEELS_BROWSER_TEST_BASE_URL) + * 5. CGI auto-detect (cgi.server_name + cgi.server_port) + * 6. Default (http://localhost:8080) + */ +component extends="wheels.WheelsTest" { + + function run() { + describe("BrowserTest base URL layered resolution (issue ##2779)", () => { + + it("honors this.baseUrl as the highest-precedence override", () => { + var bt = new wheels.wheelstest.BrowserTest(); + bt.baseUrl = "http://override.example:9999"; + expect(bt.$resolveBaseUrl()).toBe("http://override.example:9999"); + }); + + it("falls back through layers when this.baseUrl is empty", () => { + // With no per-spec override, no Wheels setting, and no JVM + // property, the env var set by test-local.sh ("http://localhost:8080") + // or the localhost:8080 default both resolve to a valid + // http(s):// URL — the bug was returning the wrong port, not + // an invalid value. + var bt = new wheels.wheelstest.BrowserTest(); + bt.baseUrl = ""; + expect(bt.$resolveBaseUrl()).toMatch("^https?://"); + }); + + it("$detectBaseUrlFromCgi returns blank when port is the stale 8080 default", () => { + var bt = new wheels.wheelstest.BrowserTest(); + var fakeCgi = {server_port: "8080", server_name: "localhost", https: "off"}; + expect(bt.$detectBaseUrlFromCgi(fakeCgi)).toBe(""); + }); + + it("$detectBaseUrlFromCgi builds the URL from a non-default port", () => { + var bt = new wheels.wheelstest.BrowserTest(); + var fakeCgi = {server_port: "60050", server_name: "localhost", https: "off"}; + expect(bt.$detectBaseUrlFromCgi(fakeCgi)).toBe("http://localhost:60050"); + }); + + it("$detectBaseUrlFromCgi honors the https scheme when cgi.https is on", () => { + var bt = new wheels.wheelstest.BrowserTest(); + var fakeCgi = {server_port: "443", server_name: "staging.example.com", https: "on"}; + expect(bt.$detectBaseUrlFromCgi(fakeCgi)).toBe("https://staging.example.com:443"); + }); + + it("$detectBaseUrlFromCgi returns blank when server_port is missing or zero", () => { + var bt = new wheels.wheelstest.BrowserTest(); + expect(bt.$detectBaseUrlFromCgi({server_port: "0"})).toBe(""); + expect(bt.$detectBaseUrlFromCgi({})).toBe(""); + }); + + it("JVM system property is honored when this.baseUrl is empty", () => { + var bt = new wheels.wheelstest.BrowserTest(); + bt.baseUrl = ""; + var sys = createObject("java", "java.lang.System"); + var key = "wheels.browserTest.baseUrl"; + var prior = sys.getProperty(key); + try { + sys.setProperty(key, "http://jvm-prop.example:1234"); + expect(bt.$resolveBaseUrl()).toBe("http://jvm-prop.example:1234"); + } finally { + if (isNull(prior)) { + sys.clearProperty(key); + } else { + sys.setProperty(key, prior); + } + } + }); + + }); + } +} diff --git a/vendor/wheels/wheelstest/BrowserTest.cfc b/vendor/wheels/wheelstest/BrowserTest.cfc index 10d6859eba..6802d89cfd 100644 --- a/vendor/wheels/wheelstest/BrowserTest.cfc +++ b/vendor/wheels/wheelstest/BrowserTest.cfc @@ -40,6 +40,10 @@ component extends="wheels.WheelsTest" { this.browserScreenshotOnFailure = true; this.browser = ""; this.browserTestSkipped = false; + // Per-spec base-URL override (issue #2779). Highest-precedence layer in + // $resolveBaseUrl(). Specs targeting a non-default port can set this in + // beforeAll() instead of relying on the JVM-cached env var. + this.baseUrl = ""; variables.$launcher = ""; variables.$browser = ""; @@ -228,26 +232,72 @@ component extends="wheels.WheelsTest" { } /** - * Base URL for `client.visit("/path")`. Defaults to localhost:8080 - * (the default LuCLI port); override via WHEELS_BROWSER_TEST_BASE_URL env. + * Base URL for `client.visit("/path")`. Resolved through a layered lookup + * at instance time so post-launch overrides take effect (the JVM caches + * env vars at process start, so `export`-ing after `wheels start` is too + * late for the bare-env-var approach). Precedence, highest first: + * + * 1. this.baseUrl — per-spec override + * 2. get("browserTestBaseUrl") — Wheels setting + * 3. -Dwheels.browserTest.baseUrl=... — JVM system property + * 4. WHEELS_BROWSER_TEST_BASE_URL env — CI / shell + * 5. $detectBaseUrlFromCgi(cgi) — derived from the in-flight + * test-runner request + * 6. "http://localhost:8080" default — bare LuCLI port + * * For specs that use only `visitUrl(absolute)` (e.g. data:/file:), the * baseUrl is effectively unused. */ - private string function $resolveBaseUrl() { + public string function $resolveBaseUrl() { + if (len(this.baseUrl ?: "")) return this.baseUrl; + + try { + var setting = get(name="browserTestBaseUrl"); + if (len(setting ?: "")) return setting; + } catch (any e) { + // Setting not registered — fall through to next layer. + } + try { - var env = createObject("java", "java.lang.System") - .getenv("WHEELS_BROWSER_TEST_BASE_URL"); + var sys = createObject("java", "java.lang.System"); + var prop = sys.getProperty("wheels.browserTest.baseUrl"); + if (!isNull(prop) && len(prop)) return prop; + var env = sys.getenv("WHEELS_BROWSER_TEST_BASE_URL"); if (!isNull(env) && len(env)) return env; } catch (any e) { - // Best-effort: SecurityManager could deny env access. Falling - // back to the localhost default is correct — if the user actually - // configured a different URL but we can't read it, their tests - // will fail with connection-refused, surfacing the problem - // clearly rather than silently using the wrong URL. + // Best-effort: a SecurityManager could deny system access. + // Falling through to CGI detection / default is correct. } + + try { + var detected = $detectBaseUrlFromCgi(cgi); + if (len(detected)) return detected; + } catch (any e) { + // cgi scope unavailable (rare; e.g. background thread) — fall + // through to the hardcoded default. + } + return "http://localhost:8080"; } + /** + * Derive base URL from the in-flight test-runner request. The core test + * suite runs over HTTP at /wheels/core/tests, so the cgi scope already + * names the correct host:port for the running server. Returns "" when + * cgi looks like the bare LuCLI default (localhost:8080) so downstream + * layers can supply the real value. + */ + public string function $detectBaseUrlFromCgi(required any cgiScope) { + if (!structKeyExists(arguments.cgiScope, "server_port") || !val(arguments.cgiScope.server_port ?: 0)) { + return ""; + } + var port = val(arguments.cgiScope.server_port); + var host = len(arguments.cgiScope.server_name ?: "") ? arguments.cgiScope.server_name : "localhost"; + var scheme = (arguments.cgiScope.https ?: "off") == "on" ? "https" : "http"; + if (host == "localhost" && port == 8080) return ""; + return scheme & "://" & host & ":" & port; + } + /** * Best-effort capture of screenshot + HTML on test failure. * Called from aroundEach catch block. Swallows all errors to avoid From c1e542e4bb025ce64fe632f4499c31e888f770d2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 19:25:38 +0000 Subject: [PATCH 2/3] docs(web/guides): document BrowserTest layered base-URL resolution (#2779) Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .ai/wheels/testing/browser-testing.md | 2 +- .../guides/src/content/docs/v4-0-0/testing/browser-tests.mdx | 4 ++-- .../content/docs/v4-0-1-snapshot/testing/browser-tests.mdx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.ai/wheels/testing/browser-testing.md b/.ai/wheels/testing/browser-testing.md index 8de36ff461..3068c825eb 100644 --- a/.ai/wheels/testing/browser-testing.md +++ b/.ai/wheels/testing/browser-testing.md @@ -63,6 +63,6 @@ bash tools/test-local.sh # skips browser specs if JARs missin - **`client` is a Lucee reserved scope.** `var client = ...` in a closure throws "client scope is not enabled". Use `var c = ...` or `var bc = ...`. (Generalized rule: see CLAUDE.md anti-pattern #11.) - **Data URLs work for most tests** — no server needed for ~95% of DSL coverage. Full HTTP integration (cookies, form submits, redirects) needs a running fixture app; that wiring is the same as Wheels Web app bootstrap (separate server + baseUrl). - **`this.browserTestSkipped`** — when Playwright JARs aren't installed (fresh CI, clean machine), `beforeAll` sets this flag and `browserDescribe`'s hooks short-circuit. All `it`s should check `if (this.browserTestSkipped) return;` to stay green on CI. -- **CI runs browser tests** — `pr.yml` and `snapshot.yml` install Playwright JARs + Chromium (cached via `browser-manifest.json` hash). Browser specs run as part of the normal test suite. `WHEELS_BROWSER_TEST_BASE_URL=http://localhost:60007` is set automatically. +- **CI runs browser tests** — `pr.yml` and `snapshot.yml` install Playwright JARs + Chromium (cached via `browser-manifest.json` hash). Browser specs run as part of the normal test suite. `WHEELS_BROWSER_TEST_BASE_URL=http://localhost:60007` is set automatically. The base URL is resolved at instance time through a layered lookup (`this.baseUrl` → Wheels setting → JVM property `wheels.browserTest.baseUrl` → env var → CGI auto-detect → `http://localhost:8080`); per-spec `this.baseUrl` set in `beforeAll()` takes priority over the env var. - **Fixture routes** — `/_browser/login-as` and `/_browser/logout` are mounted automatically in test mode. They must come before `.wildcard()` in routes.cfm. In the Routes UI (`/wheels/routes`) all `/_browser/*` routes appear under the **Internal** tab, not Application. - **Dialogs are Lucee-only** — `acceptDialog`, `dismissDialog`, `dialogMessage` use `createDynamicProxy` which is Lucee-specific. Specs skip gracefully on other engines. diff --git a/web/sites/guides/src/content/docs/v4-0-0/testing/browser-tests.mdx b/web/sites/guides/src/content/docs/v4-0-0/testing/browser-tests.mdx index 99c54e85f6..89a90b82b6 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/testing/browser-tests.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/testing/browser-tests.mdx @@ -96,7 +96,7 @@ component extends="wheels.wheelstest.BrowserTest" { `this.browser` is a `BrowserClient`. Every method that mutates state returns `this`, so calls chain fluently until you hit a terminal (a method that returns a value — `currentUrl()`, `text()`, etc.). -- `visit(path)` — navigate to a path joined to the base URL (`http://localhost:8080` by default, or `WHEELS_BROWSER_TEST_BASE_URL`). Path must start with `/`. +- `visit(path)` — navigate to a path joined to the base URL, resolved at instance time through a layered lookup: `this.baseUrl` (per-spec override) → `get("browserTestBaseUrl")` (Wheels setting) → JVM system property `wheels.browserTest.baseUrl` → `WHEELS_BROWSER_TEST_BASE_URL` env var → CGI auto-detect → `http://localhost:8080` default. Path must start with `/`. - `visitUrl(url)` — navigate to an absolute URL. Use for `data:`, `file:`, or cross-origin tests. - `visitRoute(route, key, params)` — resolve a named route via `URLFor()` and navigate there. Requires the Wheels app to be booted. - `back()` / `forward()` / `refresh()` — drive the browser history. @@ -316,7 +316,7 @@ When a spec fails, the `aroundEach` hook captures a screenshot and HTML dump to The CI workflow sets `WHEELS_CI=true`, which `BrowserTest.beforeAll` interprets as an opt-out from running browser specs (they're skipped via `this.browserTestSkipped = true`). To run browser specs in CI, also set `WHEELS_BROWSER_CI_ENABLE=true` and ensure Playwright is installed on the runner. CI caches the Playwright JARs and Chromium via the `browser-manifest.json` hash; first runs pay the download cost once. -Set `WHEELS_BROWSER_TEST_BASE_URL` so `visit("/path")` resolves to the right origin on the CI runner. For local work, the default `http://localhost:8080` matches `wheels server run --port=8080`. See [CI Integration](/v4-0-0/testing/ci-integration/) for the full workflow. +Set `WHEELS_BROWSER_TEST_BASE_URL`, set `this.baseUrl` in `beforeAll`, or rely on CGI auto-detect so `visit("/path")` resolves to the right origin. The env var is the standard CI escape hatch; `this.baseUrl` takes priority and lets individual specs target a different port without touching the environment. For local work against a non-8080 server (Titan on 60050, `wheels new` scaffolds on 60080), CGI auto-detect picks up the correct host and port automatically. See [CI Integration](/v4-0-0/testing/ci-integration/) for the full workflow. ## When to skip browser tests diff --git a/web/sites/guides/src/content/docs/v4-0-1-snapshot/testing/browser-tests.mdx b/web/sites/guides/src/content/docs/v4-0-1-snapshot/testing/browser-tests.mdx index bea6eeb265..4ef1829af4 100644 --- a/web/sites/guides/src/content/docs/v4-0-1-snapshot/testing/browser-tests.mdx +++ b/web/sites/guides/src/content/docs/v4-0-1-snapshot/testing/browser-tests.mdx @@ -96,7 +96,7 @@ component extends="wheels.wheelstest.BrowserTest" { `this.browser` is a `BrowserClient`. Every method that mutates state returns `this`, so calls chain fluently until you hit a terminal (a method that returns a value — `currentUrl()`, `text()`, etc.). -- `visit(path)` — navigate to a path joined to the base URL (`http://localhost:8080` by default, or `WHEELS_BROWSER_TEST_BASE_URL`). Path must start with `/`. +- `visit(path)` — navigate to a path joined to the base URL, resolved at instance time through a layered lookup: `this.baseUrl` (per-spec override) → `get("browserTestBaseUrl")` (Wheels setting) → JVM system property `wheels.browserTest.baseUrl` → `WHEELS_BROWSER_TEST_BASE_URL` env var → CGI auto-detect → `http://localhost:8080` default. Path must start with `/`. - `visitUrl(url)` — navigate to an absolute URL. Use for `data:`, `file:`, or cross-origin tests. - `visitRoute(route, key, params)` — resolve a named route via `URLFor()` and navigate there. Requires the Wheels app to be booted. - `back()` / `forward()` / `refresh()` — drive the browser history. @@ -316,7 +316,7 @@ When a spec fails, the `aroundEach` hook captures a screenshot and HTML dump to The CI workflow sets `WHEELS_CI=true`, which `BrowserTest.beforeAll` interprets as an opt-out from running browser specs (they're skipped via `this.browserTestSkipped = true`). To run browser specs in CI, also set `WHEELS_BROWSER_CI_ENABLE=true` and ensure Playwright is installed on the runner. CI caches the Playwright JARs and Chromium via the `browser-manifest.json` hash; first runs pay the download cost once. -Set `WHEELS_BROWSER_TEST_BASE_URL` so `visit("/path")` resolves to the right origin on the CI runner. For local work, the default `http://localhost:8080` matches `wheels server run --port=8080`. See [CI Integration](/v4-0-1-snapshot/testing/ci-integration/) for the full workflow. +Set `WHEELS_BROWSER_TEST_BASE_URL`, set `this.baseUrl` in `beforeAll`, or rely on CGI auto-detect so `visit("/path")` resolves to the right origin. The env var is the standard CI escape hatch; `this.baseUrl` takes priority and lets individual specs target a different port without touching the environment. For local work against a non-8080 server (Titan on 60050, `wheels new` scaffolds on 60080), CGI auto-detect picks up the correct host and port automatically. See [CI Integration](/v4-0-1-snapshot/testing/ci-integration/) for the full workflow. ## When to skip browser tests From 9bb88254786b2c9a028ddf56cd93cf5f1ec56b02 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 19:39:25 +0000 Subject: [PATCH 3/3] fix(test): address Reviewer A/B consensus findings (round 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Strip canonical ports (http:80, https:443) in $detectBaseUrlFromCgi to match URL conventions; update the https:443 spec expectation and add a dedicated http:80 case (vendor/wheels/wheelstest/BrowserTest.cfc:297-299, vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc:51-61). - Document why the "falls back through layers" assertion is intentionally weak — JVM env vars are read-only from CFML and the Wheels get() setting needs a live framework context, so layer isolation isn't fully testable at that level (vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc:28-43). - Update browser-test guides (v4-0-0 + v4-0-1-snapshot L319) to recommend setting this.baseUrl in the component pseudo-constructor instead of beforeAll — super.beforeAll() calls $resolveBaseUrl() before a beforeAll-override can take effect, silently inheriting layer 2-6 results. - Mirror the same ordering note in .ai/wheels/testing/browser-testing.md. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .ai/wheels/testing/browser-testing.md | 2 +- .../BrowserTestBaseUrlResolutionSpec.cfc | 21 +++++++++++++------ vendor/wheels/wheelstest/BrowserTest.cfc | 3 ++- .../docs/v4-0-0/testing/browser-tests.mdx | 14 ++++++++++++- .../v4-0-1-snapshot/testing/browser-tests.mdx | 14 ++++++++++++- 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/.ai/wheels/testing/browser-testing.md b/.ai/wheels/testing/browser-testing.md index 3068c825eb..66d8918fcf 100644 --- a/.ai/wheels/testing/browser-testing.md +++ b/.ai/wheels/testing/browser-testing.md @@ -63,6 +63,6 @@ bash tools/test-local.sh # skips browser specs if JARs missin - **`client` is a Lucee reserved scope.** `var client = ...` in a closure throws "client scope is not enabled". Use `var c = ...` or `var bc = ...`. (Generalized rule: see CLAUDE.md anti-pattern #11.) - **Data URLs work for most tests** — no server needed for ~95% of DSL coverage. Full HTTP integration (cookies, form submits, redirects) needs a running fixture app; that wiring is the same as Wheels Web app bootstrap (separate server + baseUrl). - **`this.browserTestSkipped`** — when Playwright JARs aren't installed (fresh CI, clean machine), `beforeAll` sets this flag and `browserDescribe`'s hooks short-circuit. All `it`s should check `if (this.browserTestSkipped) return;` to stay green on CI. -- **CI runs browser tests** — `pr.yml` and `snapshot.yml` install Playwright JARs + Chromium (cached via `browser-manifest.json` hash). Browser specs run as part of the normal test suite. `WHEELS_BROWSER_TEST_BASE_URL=http://localhost:60007` is set automatically. The base URL is resolved at instance time through a layered lookup (`this.baseUrl` → Wheels setting → JVM property `wheels.browserTest.baseUrl` → env var → CGI auto-detect → `http://localhost:8080`); per-spec `this.baseUrl` set in `beforeAll()` takes priority over the env var. +- **CI runs browser tests** — `pr.yml` and `snapshot.yml` install Playwright JARs + Chromium (cached via `browser-manifest.json` hash). Browser specs run as part of the normal test suite. `WHEELS_BROWSER_TEST_BASE_URL=http://localhost:60007` is set automatically. The base URL is resolved at instance time through a layered lookup (`this.baseUrl` → Wheels setting → JVM property `wheels.browserTest.baseUrl` → env var → CGI auto-detect → `http://localhost:8080`); per-spec `this.baseUrl` takes priority over the env var. Set `this.baseUrl` in the component pseudo-constructor (outside any function), not inside `beforeAll()` — `super.beforeAll()` calls `$resolveBaseUrl()` and caches the result, so a `this.baseUrl =` assignment that runs after `super.beforeAll()` is silently ignored. - **Fixture routes** — `/_browser/login-as` and `/_browser/logout` are mounted automatically in test mode. They must come before `.wildcard()` in routes.cfm. In the Routes UI (`/wheels/routes`) all `/_browser/*` routes appear under the **Internal** tab, not Application. - **Dialogs are Lucee-only** — `acceptDialog`, `dismissDialog`, `dialogMessage` use `createDynamicProxy` which is Lucee-specific. Specs skip gracefully on other engines. diff --git a/vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc b/vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc index 1f354e0d4b..e9e035376f 100644 --- a/vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc +++ b/vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc @@ -26,11 +26,14 @@ component extends="wheels.WheelsTest" { }); it("falls back through layers when this.baseUrl is empty", () => { - // With no per-spec override, no Wheels setting, and no JVM - // property, the env var set by test-local.sh ("http://localhost:8080") - // or the localhost:8080 default both resolve to a valid - // http(s):// URL — the bug was returning the wrong port, not - // an invalid value. + // Intentionally weak assertion: JVM env vars are read-only + // from CFML and the Wheels get() setting requires a live + // framework context, so we can't fully isolate which layer + // (2–6) fires here. The JVM-property test below and the + // direct $detectBaseUrlFromCgi tests cover layers 3 and 5 + // in isolation. This case only verifies the chain + // terminates with a valid http(s):// URL — the original bug + // was returning the wrong port, not an invalid value. var bt = new wheels.wheelstest.BrowserTest(); bt.baseUrl = ""; expect(bt.$resolveBaseUrl()).toMatch("^https?://"); @@ -51,7 +54,13 @@ component extends="wheels.WheelsTest" { it("$detectBaseUrlFromCgi honors the https scheme when cgi.https is on", () => { var bt = new wheels.wheelstest.BrowserTest(); var fakeCgi = {server_port: "443", server_name: "staging.example.com", https: "on"}; - expect(bt.$detectBaseUrlFromCgi(fakeCgi)).toBe("https://staging.example.com:443"); + expect(bt.$detectBaseUrlFromCgi(fakeCgi)).toBe("https://staging.example.com"); + }); + + it("$detectBaseUrlFromCgi omits canonical http port 80", () => { + var bt = new wheels.wheelstest.BrowserTest(); + var fakeCgi = {server_port: "80", server_name: "example.com", https: "off"}; + expect(bt.$detectBaseUrlFromCgi(fakeCgi)).toBe("http://example.com"); }); it("$detectBaseUrlFromCgi returns blank when server_port is missing or zero", () => { diff --git a/vendor/wheels/wheelstest/BrowserTest.cfc b/vendor/wheels/wheelstest/BrowserTest.cfc index 6802d89cfd..ed94dbd1cf 100644 --- a/vendor/wheels/wheelstest/BrowserTest.cfc +++ b/vendor/wheels/wheelstest/BrowserTest.cfc @@ -295,7 +295,8 @@ component extends="wheels.WheelsTest" { var host = len(arguments.cgiScope.server_name ?: "") ? arguments.cgiScope.server_name : "localhost"; var scheme = (arguments.cgiScope.https ?: "off") == "on" ? "https" : "http"; if (host == "localhost" && port == 8080) return ""; - return scheme & "://" & host & ":" & port; + var isCanonicalPort = (scheme == "http" && port == 80) || (scheme == "https" && port == 443); + return scheme & "://" & host & (isCanonicalPort ? "" : ":" & port); } /** diff --git a/web/sites/guides/src/content/docs/v4-0-0/testing/browser-tests.mdx b/web/sites/guides/src/content/docs/v4-0-0/testing/browser-tests.mdx index 89a90b82b6..09ae990d7f 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/testing/browser-tests.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/testing/browser-tests.mdx @@ -316,7 +316,19 @@ When a spec fails, the `aroundEach` hook captures a screenshot and HTML dump to The CI workflow sets `WHEELS_CI=true`, which `BrowserTest.beforeAll` interprets as an opt-out from running browser specs (they're skipped via `this.browserTestSkipped = true`). To run browser specs in CI, also set `WHEELS_BROWSER_CI_ENABLE=true` and ensure Playwright is installed on the runner. CI caches the Playwright JARs and Chromium via the `browser-manifest.json` hash; first runs pay the download cost once. -Set `WHEELS_BROWSER_TEST_BASE_URL`, set `this.baseUrl` in `beforeAll`, or rely on CGI auto-detect so `visit("/path")` resolves to the right origin. The env var is the standard CI escape hatch; `this.baseUrl` takes priority and lets individual specs target a different port without touching the environment. For local work against a non-8080 server (Titan on 60050, `wheels new` scaffolds on 60080), CGI auto-detect picks up the correct host and port automatically. See [CI Integration](/v4-0-0/testing/ci-integration/) for the full workflow. +Set `WHEELS_BROWSER_TEST_BASE_URL`, set `this.baseUrl` in the component pseudo-constructor, or rely on CGI auto-detect so `visit("/path")` resolves to the right origin. The env var is the standard CI escape hatch; `this.baseUrl` takes priority and lets individual specs target a different port without touching the environment. Set it in the pseudo-constructor — outside any function — so it is in place before `super.beforeAll()` runs `$resolveBaseUrl()` and caches the value: + +```cfm +component extends="wheels.wheelstest.BrowserTest" { + this.baseUrl = "http://staging:60050"; // pseudo-constructor — set before super.beforeAll() + + function run() { + // specs ... + } +} +``` + +For local work against a non-8080 server (Titan on 60050, `wheels new` scaffolds on 60080), CGI auto-detect picks up the correct host and port automatically. See [CI Integration](/v4-0-0/testing/ci-integration/) for the full workflow. ## When to skip browser tests diff --git a/web/sites/guides/src/content/docs/v4-0-1-snapshot/testing/browser-tests.mdx b/web/sites/guides/src/content/docs/v4-0-1-snapshot/testing/browser-tests.mdx index 4ef1829af4..c20b4aa8bd 100644 --- a/web/sites/guides/src/content/docs/v4-0-1-snapshot/testing/browser-tests.mdx +++ b/web/sites/guides/src/content/docs/v4-0-1-snapshot/testing/browser-tests.mdx @@ -316,7 +316,19 @@ When a spec fails, the `aroundEach` hook captures a screenshot and HTML dump to The CI workflow sets `WHEELS_CI=true`, which `BrowserTest.beforeAll` interprets as an opt-out from running browser specs (they're skipped via `this.browserTestSkipped = true`). To run browser specs in CI, also set `WHEELS_BROWSER_CI_ENABLE=true` and ensure Playwright is installed on the runner. CI caches the Playwright JARs and Chromium via the `browser-manifest.json` hash; first runs pay the download cost once. -Set `WHEELS_BROWSER_TEST_BASE_URL`, set `this.baseUrl` in `beforeAll`, or rely on CGI auto-detect so `visit("/path")` resolves to the right origin. The env var is the standard CI escape hatch; `this.baseUrl` takes priority and lets individual specs target a different port without touching the environment. For local work against a non-8080 server (Titan on 60050, `wheels new` scaffolds on 60080), CGI auto-detect picks up the correct host and port automatically. See [CI Integration](/v4-0-1-snapshot/testing/ci-integration/) for the full workflow. +Set `WHEELS_BROWSER_TEST_BASE_URL`, set `this.baseUrl` in the component pseudo-constructor, or rely on CGI auto-detect so `visit("/path")` resolves to the right origin. The env var is the standard CI escape hatch; `this.baseUrl` takes priority and lets individual specs target a different port without touching the environment. Set it in the pseudo-constructor — outside any function — so it is in place before `super.beforeAll()` runs `$resolveBaseUrl()` and caches the value: + +```cfm +component extends="wheels.wheelstest.BrowserTest" { + this.baseUrl = "http://staging:60050"; // pseudo-constructor — set before super.beforeAll() + + function run() { + // specs ... + } +} +``` + +For local work against a non-8080 server (Titan on 60050, `wheels new` scaffolds on 60080), CGI auto-detect picks up the correct host and port automatically. See [CI Integration](/v4-0-1-snapshot/testing/ci-integration/) for the full workflow. ## When to skip browser tests