From f6a9c1647cf007cfb317ecef76458137a0957f62 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 03:10:33 +0000 Subject: [PATCH 1/3] fix(test): resolve app test-runner include under URL subpath installs The scaffolded tests/runner.cfm hardcoded an absolute include of /wheels/tests/app-runner.cfm, which only resolves when the app is mounted at the web root. Under a URL subpath / CommandBox multi-subfolder topology the /wheels mapping does not resolve, so /wheels/app/tests and 'wheels test' failed. Adds $resolveSubpathInclude() on Global.cfc, which prefixes a framework-relative include path with the app's resolved webPath (the same subpath derivation as $resolveFrameworkPaths), and points the app test-runner template at it. Behaviour is unchanged for root installs (webPath '/'). Refs #3251 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .../3251-app-test-runner-subpath.fixed.md | 1 + cli/lucli/templates/app/tests/runner.cfm | 7 +- vendor/wheels/Global.cfc | 28 +++++++ .../global/resolveSubpathIncludeSpec.cfc | 74 +++++++++++++++++++ 4 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3251-app-test-runner-subpath.fixed.md create mode 100644 vendor/wheels/tests/specs/global/resolveSubpathIncludeSpec.cfc diff --git a/changelog.d/3251-app-test-runner-subpath.fixed.md b/changelog.d/3251-app-test-runner-subpath.fixed.md new file mode 100644 index 0000000000..57c3b2463a --- /dev/null +++ b/changelog.d/3251-app-test-runner-subpath.fixed.md @@ -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) diff --git a/cli/lucli/templates/app/tests/runner.cfm b/cli/lucli/templates/app/tests/runner.cfm index 4155eb08ed..11f3ce60e8 100644 --- a/cli/lucli/templates/app/tests/runner.cfm +++ b/cli/lucli/templates/app/tests/runner.cfm @@ -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). ---> - + diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index 8be7b42a6c..300ea910dd 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -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. */ diff --git a/vendor/wheels/tests/specs/global/resolveSubpathIncludeSpec.cfc b/vendor/wheels/tests/specs/global/resolveSubpathIncludeSpec.cfc new file mode 100644 index 0000000000..2822dabc8f --- /dev/null +++ b/vendor/wheels/tests/specs/global/resolveSubpathIncludeSpec.cfc @@ -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 + // ``. 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") + }) + + }) + } +} From a67ee6ea716713f376e87e879934c69d36bbba54 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 03:15:21 +0000 Subject: [PATCH 2/3] docs(web/guides): reconcile testing index with wheels new scaffold output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/TestRunner.cfc is not part of the scaffolded test layout; replace it with runner.cfm (the actual scaffolded HTTP entry point). Add an Aside noting that only controllers/, functional/, and models/ are created by `wheels new` — view/ and browser/ must be created manually (#3251). Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- web/sites/guides/src/content/docs/v4-0-0/testing/index.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/sites/guides/src/content/docs/v4-0-0/testing/index.mdx b/web/sites/guides/src/content/docs/v4-0-0/testing/index.mdx index 3895ae97db..e5e569964d 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/testing/index.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/testing/index.mdx @@ -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 | + + 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 @@ -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//` | Where your actual specs live. | From 76a0a26ca42122b87a25556a7acd3b3b88b4015f Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Wed, 24 Jun 2026 09:17:36 -0700 Subject: [PATCH 3/3] test(global): cover $resolveSubpathInclude production webPath fallback Signed-off-by: Peter Amiri --- .../specs/global/resolveSubpathIncludeSpec.cfc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/vendor/wheels/tests/specs/global/resolveSubpathIncludeSpec.cfc b/vendor/wheels/tests/specs/global/resolveSubpathIncludeSpec.cfc index 2822dabc8f..0494645202 100644 --- a/vendor/wheels/tests/specs/global/resolveSubpathIncludeSpec.cfc +++ b/vendor/wheels/tests/specs/global/resolveSubpathIncludeSpec.cfc @@ -69,6 +69,20 @@ component extends="wheels.WheelsTest" { ).toBe("/wheelsproject1/wheels/tests/app-runner.cfm") }) + it("uses application.wheels.webPath when no webPath argument is passed (the production call shape)", () => { + // The shipped runner template (cli/lucli/templates/app/tests/runner.cfm) + // calls this with NO webPath argument, so the fallback branch that reads + // application.wheels.webPath is the only path real callers take. Pin it by + // asserting the no-arg result equals an explicit call passing the current + // webPath — this exercises the previously-uncovered branch without mutating + // global app state. + expect( + g.$resolveSubpathInclude(template = "/wheels/tests/app-runner.cfm") + ).toBe( + g.$resolveSubpathInclude(template = "/wheels/tests/app-runner.cfm", webPath = application.wheels.webPath) + ) + }) + }) } }