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 000000000..57c3b2463 --- /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 4155eb08e..11f3ce60e 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 8be7b42a6..300ea910d 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 000000000..2822dabc8 --- /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") + }) + + }) + } +} 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 3895ae97d..e5e569964 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. |