Skip to content

Commit f2bb8de

Browse files
fix(test): resolve app test-runner include under URL subpath installs (#3255)
Adds $resolveSubpathInclude() on Global.cfc to prefix the framework-relative app test-runner include with the app's resolved webPath, so /wheels/app/tests and `wheels test` work under URL-subpath / CommandBox multi-subfolder installs (item 2 of #3251). Pure helper with full unit coverage in resolveSubpathIncludeSpec.cfc, including the no-arg production call shape. Refs #3251 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c7ca961 commit f2bb8de

5 files changed

Lines changed: 128 additions & 2 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- The scaffolded `tests/runner.cfm` now resolves its include of the built-in app test runner through `$resolveSubpathInclude()` instead of a hardcoded absolute `/wheels/tests/app-runner.cfm` path. Under a URL subpath / CommandBox multi-subfolder install the bare `/wheels` mapping did not resolve, so `/wheels/app/tests` and `wheels test` failed; the include is now prefixed with the app's resolved `webPath` and works at the web root and under a subfolder alike (#3251, refs #2887)

cli/lucli/templates/app/tests/runner.cfm

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,10 @@
1212
Keep the include below as the last line (or replicate its body
1313
inline) — the framework runner is what produces the JSON / HTML
1414
output the rest of the system expects.
15+
16+
The include path is resolved through $resolveSubpathInclude so it
17+
works both at the web root and under a URL subpath / CommandBox
18+
multi-subfolder install, where a bare `/wheels/...` mapping does not
19+
resolve (issue #3251).
1520
--->
16-
<cfinclude template="/wheels/tests/app-runner.cfm">
21+
<cfinclude template="#application.wo.$resolveSubpathInclude('/wheels/tests/app-runner.cfm')#">

vendor/wheels/Global.cfc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2622,6 +2622,34 @@ return local.$wheels;
26222622
return local.rv;
26232623
}
26242624

2625+
/**
2626+
* Internal function. Rewrites a framework-relative include path (e.g.
2627+
* `/wheels/tests/app-runner.cfm`) so it resolves under a URL subpath
2628+
* install (issue #3251). The shipped app test-runner template includes
2629+
* the built-in app runner via an absolute `/wheels/...` path, which only
2630+
* resolves when the app is mounted at the web root; under a CommandBox
2631+
* multi-subfolder / IIS-subfolder topology the `/wheels` mapping does not
2632+
* resolve and the include fails. Prefixing the resolved `webPath` (the
2633+
* same subpath derivation as $resolveFrameworkPaths) makes the include
2634+
* work in both root and subfolder installs. Pure so it can be unit-tested
2635+
* in isolation.
2636+
*/
2637+
public string function $resolveSubpathInclude(required string template, string webPath) {
2638+
// Default to the app's resolved webPath without a runtime default-arg
2639+
// expression (some engines evaluate those eagerly); callers in tests
2640+
// pass webPath explicitly.
2641+
local.wp = StructKeyExists(arguments, "webPath") ? arguments.webPath : application.wheels.webPath;
2642+
local.base = Len(local.wp) ? local.wp : "/";
2643+
if (Right(local.base, 1) != "/") {
2644+
local.base &= "/";
2645+
}
2646+
// Strip any leading slash(es) from the framework-relative template so
2647+
// the join produces a single boundary slash. Anchored to the start so
2648+
// it never touches interior path separators.
2649+
local.relative = ReReplace(arguments.template, "^/+", "");
2650+
return local.base & local.relative;
2651+
}
2652+
26252653
/**
26262654
* Internal function.
26272655
*/
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
component extends="wheels.WheelsTest" {
2+
3+
function run() {
4+
5+
g = application.wo
6+
7+
describe("Tests that $resolveSubpathInclude (issue 3251)", () => {
8+
9+
// The app test-runner template ships
10+
// `<cfinclude template="/wheels/tests/app-runner.cfm">`. The leading
11+
// `/wheels` resolves only when the app is mounted at the web root.
12+
// Under a CommandBox multi-subfolder / IIS-subfolder topology the
13+
// mapping does not resolve, so the include fails. $resolveSubpathInclude
14+
// rewrites a framework-relative include path against the resolved
15+
// `webPath` (the same subpath derivation as $resolveFrameworkPaths) so
16+
// the include works in both root and subfolder installs.
17+
18+
it("returns the absolute path unchanged for a root install (webPath '/')", () => {
19+
expect(
20+
g.$resolveSubpathInclude(
21+
template = "/wheels/tests/app-runner.cfm",
22+
webPath = "/"
23+
)
24+
).toBe("/wheels/tests/app-runner.cfm")
25+
})
26+
27+
it("prefixes the subpath for a subfolder install", () => {
28+
expect(
29+
g.$resolveSubpathInclude(
30+
template = "/wheels/tests/app-runner.cfm",
31+
webPath = "/wheelsproject1/"
32+
)
33+
).toBe("/wheelsproject1/wheels/tests/app-runner.cfm")
34+
})
35+
36+
it("handles a nested subpath", () => {
37+
expect(
38+
g.$resolveSubpathInclude(
39+
template = "/wheels/tests/app-runner.cfm",
40+
webPath = "/team/site/"
41+
)
42+
).toBe("/team/site/wheels/tests/app-runner.cfm")
43+
})
44+
45+
it("normalizes a webPath missing its trailing slash", () => {
46+
expect(
47+
g.$resolveSubpathInclude(
48+
template = "/wheels/tests/app-runner.cfm",
49+
webPath = "/wheelsproject1"
50+
)
51+
).toBe("/wheelsproject1/wheels/tests/app-runner.cfm")
52+
})
53+
54+
it("falls back to '/' when webPath is empty", () => {
55+
expect(
56+
g.$resolveSubpathInclude(
57+
template = "/wheels/tests/app-runner.cfm",
58+
webPath = ""
59+
)
60+
).toBe("/wheels/tests/app-runner.cfm")
61+
})
62+
63+
it("tolerates a template that omits its leading slash", () => {
64+
expect(
65+
g.$resolveSubpathInclude(
66+
template = "wheels/tests/app-runner.cfm",
67+
webPath = "/wheelsproject1/"
68+
)
69+
).toBe("/wheelsproject1/wheels/tests/app-runner.cfm")
70+
})
71+
72+
it("uses application.wheels.webPath when no webPath argument is passed (the production call shape)", () => {
73+
// The shipped runner template (cli/lucli/templates/app/tests/runner.cfm)
74+
// calls this with NO webPath argument, so the fallback branch that reads
75+
// application.wheels.webPath is the only path real callers take. Pin it by
76+
// asserting the no-arg result equals an explicit call passing the current
77+
// webPath — this exercises the previously-uncovered branch without mutating
78+
// global app state.
79+
expect(
80+
g.$resolveSubpathInclude(template = "/wheels/tests/app-runner.cfm")
81+
).toBe(
82+
g.$resolveSubpathInclude(template = "/wheels/tests/app-runner.cfm", webPath = application.wheels.webPath)
83+
)
84+
})
85+
86+
})
87+
}
88+
}

web/sites/guides/src/content/docs/v4-0-0/testing/index.mdx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Every test in a Wheels app is a CFC under `tests/specs/`, grouped by what it exe
3131
| Functional tests | `tests/specs/functional/` | Single-feature end-to-end — middleware + route + filter + action + view |
3232
| Browser tests | `tests/specs/browser/` | Full UI flows driven by Playwright — JavaScript, Turbo, form submission |
3333

34+
<Aside>
35+
`wheels new` scaffolds `tests/specs/controllers/`, `tests/specs/functional/`, and `tests/specs/models/`. Create `tests/specs/view/` and `tests/specs/browser/` yourself when you add those kinds of tests.
36+
</Aside>
37+
3438
Model and controller tests are the bulk of a healthy suite. Integration tests cover journeys that span multiple actions and users. Functional tests verify one feature end-to-end through the full request pipeline. Browser tests cover what only exists in the browser — JavaScript-driven UI, Turbo Frame updates, anything where the server's response alone doesn't tell you whether the feature works.
3539

3640
## What a WheelsTest spec looks like
@@ -91,7 +95,7 @@ Four files drive your test setup. Knowing what each one does saves a lot of head
9195

9296
| File | What it does |
9397
|------|--------------|
94-
| `tests/TestRunner.cfc` | Sets up shared state. Runs before and after the whole suite. |
98+
| `tests/runner.cfm` | Entry point for `/wheels/app/tests` and `wheels test`. Customise here for pre-test bootstrap — the built-in app runner handles the rest. |
9599
| `tests/populate.cfm` | Seeds test data. Runs **once per test run**, not per spec. |
96100
| `tests/_assets/models/` | Test-only models, often using `table()` to map to test tables. |
97101
| `tests/specs/<category>/` | Where your actual specs live. |

0 commit comments

Comments
 (0)