Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions changelog.d/3251-app-test-runner-subpath.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +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)
7 changes: 6 additions & 1 deletion cli/lucli/templates/app/tests/runner.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,10 @@
Keep the include below as the last line (or replicate its body
inline) — the framework runner is what produces the JSON / HTML
output the rest of the system expects.

The include path is resolved through $resolveSubpathInclude so it
works both at the web root and under a URL subpath / CommandBox
multi-subfolder install, where a bare `/wheels/...` mapping does not
resolve (issue #3251).
--->
<cfinclude template="/wheels/tests/app-runner.cfm">
<cfinclude template="#application.wo.$resolveSubpathInclude('/wheels/tests/app-runner.cfm')#">
28 changes: 28 additions & 0 deletions vendor/wheels/Global.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -2622,6 +2622,34 @@ return local.$wheels;
return local.rv;
}

/**
* Internal function. Rewrites a framework-relative include path (e.g.
* `/wheels/tests/app-runner.cfm`) so it resolves under a URL subpath
* install (issue #3251). The shipped app test-runner template includes
* the built-in app runner via an absolute `/wheels/...` path, which only
* resolves when the app is mounted at the web root; under a CommandBox
* multi-subfolder / IIS-subfolder topology the `/wheels` mapping does not
* resolve and the include fails. Prefixing the resolved `webPath` (the
* same subpath derivation as $resolveFrameworkPaths) makes the include
* work in both root and subfolder installs. Pure so it can be unit-tested
* in isolation.
*/
public string function $resolveSubpathInclude(required string template, string webPath) {
// Default to the app's resolved webPath without a runtime default-arg
// expression (some engines evaluate those eagerly); callers in tests
// pass webPath explicitly.
local.wp = StructKeyExists(arguments, "webPath") ? arguments.webPath : application.wheels.webPath;
local.base = Len(local.wp) ? local.wp : "/";
if (Right(local.base, 1) != "/") {
local.base &= "/";
}
// Strip any leading slash(es) from the framework-relative template so
// the join produces a single boundary slash. Anchored to the start so
// it never touches interior path separators.
local.relative = ReReplace(arguments.template, "^/+", "");
return local.base & local.relative;
}

/**
* Internal function.
*/
Expand Down
74 changes: 74 additions & 0 deletions vendor/wheels/tests/specs/global/resolveSubpathIncludeSpec.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
component extends="wheels.WheelsTest" {

function run() {

g = application.wo

describe("Tests that $resolveSubpathInclude (issue 3251)", () => {

// The app test-runner template ships
// `<cfinclude template="/wheels/tests/app-runner.cfm">`. The leading
// `/wheels` resolves only when the app is mounted at the web root.
// Under a CommandBox multi-subfolder / IIS-subfolder topology the
// mapping does not resolve, so the include fails. $resolveSubpathInclude
// rewrites a framework-relative include path against the resolved
// `webPath` (the same subpath derivation as $resolveFrameworkPaths) so
// the include works in both root and subfolder installs.

it("returns the absolute path unchanged for a root install (webPath '/')", () => {
expect(
g.$resolveSubpathInclude(
template = "/wheels/tests/app-runner.cfm",
webPath = "/"
)
).toBe("/wheels/tests/app-runner.cfm")
})

it("prefixes the subpath for a subfolder install", () => {
expect(
g.$resolveSubpathInclude(
template = "/wheels/tests/app-runner.cfm",
webPath = "/wheelsproject1/"
)
).toBe("/wheelsproject1/wheels/tests/app-runner.cfm")
})

it("handles a nested subpath", () => {
expect(
g.$resolveSubpathInclude(
template = "/wheels/tests/app-runner.cfm",
webPath = "/team/site/"
)
).toBe("/team/site/wheels/tests/app-runner.cfm")
})

it("normalizes a webPath missing its trailing slash", () => {
expect(
g.$resolveSubpathInclude(
template = "/wheels/tests/app-runner.cfm",
webPath = "/wheelsproject1"
)
).toBe("/wheelsproject1/wheels/tests/app-runner.cfm")
})

it("falls back to '/' when webPath is empty", () => {
expect(
g.$resolveSubpathInclude(
template = "/wheels/tests/app-runner.cfm",
webPath = ""
)
).toBe("/wheels/tests/app-runner.cfm")
})

it("tolerates a template that omits its leading slash", () => {
expect(
g.$resolveSubpathInclude(
template = "wheels/tests/app-runner.cfm",
webPath = "/wheelsproject1/"
)
).toBe("/wheelsproject1/wheels/tests/app-runner.cfm")
})

})
}
}
6 changes: 5 additions & 1 deletion web/sites/guides/src/content/docs/v4-0-0/testing/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Every test in a Wheels app is a CFC under `tests/specs/`, grouped by what it exe
| Functional tests | `tests/specs/functional/` | Single-feature end-to-end — middleware + route + filter + action + view |
| Browser tests | `tests/specs/browser/` | Full UI flows driven by Playwright — JavaScript, Turbo, form submission |

<Aside>
`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.
</Aside>

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.

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

| File | What it does |
|------|--------------|
| `tests/TestRunner.cfc` | Sets up shared state. Runs before and after the whole suite. |
| `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. |
| `tests/populate.cfm` | Seeds test data. Runs **once per test run**, not per spec. |
| `tests/_assets/models/` | Test-only models, often using `table()` to map to test tables. |
| `tests/specs/<category>/` | Where your actual specs live. |
Expand Down
Loading