Skip to content

Commit 93ba712

Browse files
fix(test): BrowserTest reports unwired this.browser with browserDescribe() hint (#2782)
* fix(test): BrowserTest reports unwired this.browser with browserDescribe() hint Plain describe() blocks inside BrowserTest subclasses left this.browser as an empty string, so the first DSL call surfaced as "function [visitUrl] does not exist in the String" — a misleading error that hits every newcomer on iteration 1. Install an UnwiredBrowserGuard sentinel at this.browser before browserDescribe() wires a real BrowserClient (and after $endBrowserContext tears it down) so any method call throws Wheels.BrowserTest.NotWired with a message naming browserDescribe() as the fix. Fixes #2778 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * docs(web/guides): note Wheels.BrowserTest.NotWired when describe() used instead of browserDescribe() Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --------- Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
1 parent 205ac08 commit 93ba712

6 files changed

Lines changed: 72 additions & 3 deletions

File tree

.ai/wheels/testing/browser-testing.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ bash tools/test-local.sh # skips browser specs if JARs missin
5959

6060
## Key gotchas
6161

62+
- **`this.browser` is not wired in plain `describe()` blocks.** Calling any DSL method on `this.browser` outside a `browserDescribe()` block throws `Wheels.BrowserTest.NotWired` (message names `browserDescribe()` as the fix; `detail` names the method that was called). The sentinel `UnwiredBrowserGuard` is installed at construction and after each `$endBrowserContext()` teardown.
6263
- **`##` in selectors** — CFML requires `##` to emit literal `#`. `"##email"``"#email"` at runtime.
6364
- **`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.)
6465
- **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).

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo
2222

2323
### Fixed
2424

25+
- `wheels.wheelstest.BrowserTest` now throws a clear `Wheels.BrowserTest.NotWired` error — naming `browserDescribe()` as the fix — when a spec calls a DSL method on `this.browser` from a plain `describe()` block. Previously the uninitialized `this.browser` was an empty string, producing the misleading `function [visitUrl] does not exist in the String` on every newcomer's first BrowserTest spec. A sentinel `UnwiredBrowserGuard` is now installed at `this.browser` before `browserDescribe()` wires the real `BrowserClient` and after `$endBrowserContext()` tears it down
2526
- 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)
2627
- `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
2728

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Self-test for the unwired-this.browser guard. A BrowserTest subclass
3+
* that uses plain describe() (instead of browserDescribe()) must surface
4+
* a clear "use browserDescribe()" error when any method is invoked on
5+
* this.browser — not "function [visitUrl] does not exist in the String".
6+
*/
7+
component extends="wheels.WheelsTest" {
8+
9+
function run() {
10+
describe("BrowserTest unwired this.browser guard", () => {
11+
12+
it("throws Wheels.BrowserTest.NotWired when a DSL method is called before browserDescribe wiring", () => {
13+
var spec = new wheels.wheelstest.BrowserTest();
14+
expect(function() {
15+
spec.browser.visitUrl("data:text/html,<h1>Hi</h1>");
16+
}).toThrow(type="Wheels.BrowserTest.NotWired");
17+
});
18+
19+
it("error message names browserDescribe() so users see the fix", () => {
20+
var spec = new wheels.wheelstest.BrowserTest();
21+
var state = {message: "", detail: ""};
22+
23+
try {
24+
spec.browser.assertSee("anything");
25+
} catch (Wheels.BrowserTest.NotWired e) {
26+
state.message = e.message;
27+
state.detail = e.detail;
28+
}
29+
30+
expect(state.message).toInclude("browserDescribe");
31+
expect(state.detail).toInclude("assertSee");
32+
});
33+
34+
});
35+
}
36+
}

vendor/wheels/wheelstest/BrowserTest.cfc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ component extends="wheels.WheelsTest" {
3838
this.browserEngine = "chromium";
3939
this.browserViewport = ""; // empty = Playwright default; "mobile"/"tablet"/"desktop" or {width:N, height:N}
4040
this.browserScreenshotOnFailure = true;
41-
this.browser = "";
41+
// Sentinel: any method call on this.browser before browserDescribe() wires
42+
// a real BrowserClient throws Wheels.BrowserTest.NotWired with a helpful
43+
// message naming browserDescribe(), instead of the misleading
44+
// "function [X] does not exist in the String".
45+
this.browser = new wheels.wheelstest.UnwiredBrowserGuard();
4246
this.browserTestSkipped = false;
4347

4448
variables.$launcher = "";
@@ -178,7 +182,7 @@ component extends="wheels.WheelsTest" {
178182
}
179183
variables.$context = "";
180184
variables.$page = "";
181-
this.browser = "";
185+
this.browser = new wheels.wheelstest.UnwiredBrowserGuard();
182186
}
183187
}
184188

@@ -256,6 +260,7 @@ component extends="wheels.WheelsTest" {
256260
public void function $captureFailureArtifacts(required any spec) {
257261
if (!(this.browserScreenshotOnFailure ?: true)) return;
258262
if (!isObject(this.browser) || this.browserTestSkipped) return;
263+
if (structKeyExists(this.browser, "$isUnwiredBrowserGuard")) return;
259264

260265
try {
261266
var artifactDir = this.browserArtifactPath
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Sentinel installed at `this.browser` before browserDescribe() wires a real
3+
* BrowserClient. Any DSL method call (visitUrl, click, assertSee, ...) raises
4+
* Wheels.BrowserTest.NotWired with a message that names the actual cause —
5+
* the spec is using plain describe() inside a BrowserTest subclass — instead
6+
* of the original misleading "function [X] does not exist in the String".
7+
*
8+
* Carries a flag `this.$isUnwiredBrowserGuard = true` so callers that need to
9+
* distinguish the guard from a real BrowserClient can use structKeyExists()
10+
* without invoking onMissingMethod.
11+
*/
12+
component {
13+
14+
this.$isUnwiredBrowserGuard = true;
15+
16+
public any function onMissingMethod(
17+
required string missingMethodName,
18+
required struct missingMethodArguments
19+
) {
20+
throw(
21+
type="Wheels.BrowserTest.NotWired",
22+
message="this.browser is not wired. BrowserTest specs must use browserDescribe() blocks instead of describe() — the framework only populates this.browser inside browserDescribe() callbacks.",
23+
detail="Attempted to call this.browser." & arguments.missingMethodName & "() outside a browserDescribe() block. Change describe(...) to browserDescribe(...) so the browser context is set up per `it` block, or do not access this.browser from this block."
24+
);
25+
}
26+
}

web/sites/guides/src/content/docs/v4-0-1-snapshot/testing/browser-tests.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Two things to note in every browser spec:
6767

6868
## `browserDescribe` vs `describe`
6969

70-
Regular `describe()` works inside a `BrowserTest` subclass — but every `it` inside it would share the same browser state, because `beforeEach`/`afterEach` aren't automatically wired. `browserDescribe()` wraps `describe()` with hooks that create a fresh Playwright `BrowserContext` and `Page` per `it`, then tear them down afterwards. Each test gets its own cookies, localStorage, and navigation history.
70+
Regular `describe()` works inside a `BrowserTest` subclass — but `this.browser` is **not wired** inside a plain `describe()` block. Calling any DSL method on it (for example, `this.browser.visitUrl(...)`) throws `Wheels.BrowserTest.NotWired` with a message that names `browserDescribe()` as the fix. If you see that error, change `describe(...)` to `browserDescribe(...)`. `browserDescribe()` wraps `describe()` with hooks that create a fresh Playwright `BrowserContext` and `Page` per `it`, then tear them down afterwards. Each test gets its own cookies, localStorage, and navigation history.
7171

7272
```cfm {test:compile} title="tests/specs/browser/IsolationSpec.cfc"
7373
component extends="wheels.wheelstest.BrowserTest" {

0 commit comments

Comments
 (0)