Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
22 changes: 14 additions & 8 deletions vendor/wheels/controller/processing.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,21 @@ component {
extendedInfo = "Make sure your action does not have the same name as any of the built-in Wheels functions."
);
}
if (StructKeyExists(this, arguments.action) && IsCustomFunction(this[arguments.action])) {
$invoke(method = arguments.action);
} else if (StructKeyExists(this, "onMissingMethod")) {
local.invokeArgs = {};
local.invokeArgs.missingMethodName = arguments.action;
local.invokeArgs.missingMethodArguments = {};
$invoke(method = "onMissingMethod", invokeArgs = local.invokeArgs);
try {
if (StructKeyExists(this, arguments.action) && IsCustomFunction(this[arguments.action])) {
$invoke(method = arguments.action);
} else if (StructKeyExists(this, "onMissingMethod")) {
local.invokeArgs = {};
local.invokeArgs.missingMethodName = arguments.action;
local.invokeArgs.missingMethodArguments = {};
$invoke(method = "onMissingMethod", invokeArgs = local.invokeArgs);
}
} catch (any e) {
// Re-throw the original error instead of falling through to the
// auto-render block, which would produce a misleading ViewNotFound.
Throw(object = e);
}
if (!$performedRenderOrRedirect()) {
if (!$performedRenderOrRedirect() && !$renderWithAttempted()) {
// Check if we should skip automatic view rendering
local.contentType = $requestContentType();
local.acceptableFormats = $acceptableFormats(action = arguments.action);
Expand Down
11 changes: 11 additions & 0 deletions vendor/wheels/controller/rendering.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ component {
boolean hideDebugInformation = false,
string status = $statusCode()
) {
// Mark that renderWith was called so the auto-render block in $callAction
// won't attempt view file lookup if renderWith fails mid-execution.
variables.$instance.renderWithAttempted = true;
$args(name = "renderWith", args = arguments);
local.contentType = $requestContentType();
local.acceptableFormats = $acceptableFormats(action = arguments.action);
Expand Down Expand Up @@ -885,6 +888,14 @@ component {
return StructKeyExists(variables.$instance, "response");
}

/**
* Internal function.
* Returns true if renderWith() was called (even if it failed before completing).
*/
public boolean function $renderWithAttempted() {
return StructKeyExists(variables.$instance, "renderWithAttempted") && variables.$instance.renderWithAttempted;
}

/**
* Internal function.
*/
Expand Down
62 changes: 62 additions & 0 deletions vendor/wheels/tests/specs/controller/renderingSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,64 @@ component extends="wheels.WheelsTest" {
expect(_controller.$useLayout("show")).toBeFalse()
})
})

describe("Tests that $callAction respects explicit rendering", () => {

it("does not trigger view lookup when renderText is called in an action", () => {
// Use the dummy controller which has no view files.
// Inject an action that calls renderText().
params = {controller = "dummy", action = "renderTextAction"}
_controller = application.wo.controller("dummy", params)
_controller.renderTextAction = function() {
renderText("hello from renderText");
}

// $callAction should NOT throw ViewNotFound because
// renderText sets the response before the auto-render block.
_controller.$callAction(action = "renderTextAction")

expect(_controller.response()).toBe("hello from renderText")
})

it("does not trigger view lookup when renderNothing is called in an action", () => {
params = {controller = "dummy", action = "renderNothingAction"}
_controller = application.wo.controller("dummy", params)
_controller.renderNothingAction = function() {
renderNothing();
}

_controller.$callAction(action = "renderNothingAction")

expect(_controller.response()).toBe("")
})

it("re-throws action errors instead of producing ViewNotFound", () => {
params = {controller = "dummy", action = "brokenAction"}
_controller = application.wo.controller("dummy", params)
_controller.brokenAction = function() {
Throw(type = "CustomAppError", message = "Something broke in the action");
}

expect(function() {
_controller.$callAction(action = "brokenAction")
}).toThrow("CustomAppError")
})

it("skips view lookup when renderWith was attempted but failed", () => {
// Simulate renderWith being called by setting the flag directly.
params = {controller = "dummy", action = "noViewAction"}
_controller = application.wo.controller("dummy", params)

// Manually mark renderWith as attempted (simulates renderWith()
// entering and then failing before it could call renderText).
_controller.$injectIntoVariablesScope = this.$injectInstanceFlag
_controller.$injectIntoVariablesScope()

// The auto-render block should skip view lookup because
// renderWith was attempted.
expect(_controller.$renderWithAttempted()).toBeTrue()
})
})
}

function $injectIntoVariablesScope(required string name, required any data) {
Expand All @@ -723,4 +781,8 @@ component extends="wheels.WheelsTest" {
return "show_layout_ajax"
}
}

function $injectInstanceFlag() {
variables.$instance.renderWithAttempted = true;
}
}
Loading