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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "route-engine-js",
"version": "1.20.2",
"version": "1.20.3",
"description": "A lightweight Visual Novel engine built in JavaScript for creating interactive narrative games with branching storylines",
"repository": {
"type": "git",
Expand Down
62 changes: 20 additions & 42 deletions spec/RouteEngine.form.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,11 @@ const createProjectData = () => ({
{
id: "submit-button",
type: "rect",
formRole: "submit",
x: 100,
y: 230,
width: 120,
height: 48,
click: {
payload: {
actions: "${form.submitActions}",
},
},
},
{
id: "cancel-button",
type: "rect",
x: 240,
y: 230,
width: 120,
height: 48,
click: {
payload: {
actions: "${form.cancelActions}",
},
},
},
],
},
Expand Down Expand Up @@ -95,6 +78,7 @@ const createProjectData = () => ({
id: "line1",
actions: {
form: {
id: "profile-contact-form",
resourceId: "profileForm",
fields: {
name: {
Expand All @@ -113,9 +97,6 @@ const createProjectData = () => ({
submitActions: {
nextLine: {},
},
cancelActions: {
rollbackByOffset: {},
},
},
},
},
Expand Down Expand Up @@ -171,13 +152,11 @@ const findElement = (node, id) => {
};

describe("RouteEngine forms", () => {
it("renders form inputs with field drafts and prepared submit/cancel actions", () => {
it("renders form inputs with field drafts and formRole submit actions", () => {
const engine = createEngine();
const renderState = engine.selectRenderState();
const nameInput = findElement(renderState.elements, "name-input");
const submitButton = findElement(renderState.elements, "submit-button");
const cancelButton = findElement(renderState.elements, "cancel-button");

expect(nameInput).toMatchObject({
type: "input",
value: "",
Expand All @@ -189,7 +168,6 @@ describe("RouteEngine forms", () => {
updateFormField: {
field: "name",
value: "_event.value",
_interactionSource: "form",
},
},
},
Expand All @@ -199,37 +177,36 @@ describe("RouteEngine forms", () => {
_interactionSource: "form",
actions: {
submitForm: {
formId: "profileForm",
formKey: "section1:line1:profile-contact-form",
},
},
},
},
});
expect(nameInput.change.payload._formKey).toBe(
"section1:line1:profileForm",
expect(nameInput.change.payload).not.toHaveProperty("_formId");
expect(nameInput.change.payload).not.toHaveProperty("_formKey");
expect(nameInput.change.payload.actions.updateFormField.formKey).toBe(
"section1:line1:profile-contact-form",
);
expect(nameInput.change.payload.actions.updateFormField).not.toHaveProperty(
"formId",
);
expect(submitButton.click.payload).not.toHaveProperty("_formId");
expect(submitButton.click.payload).not.toHaveProperty("_formKey");
expect(submitButton.click.payload).toMatchObject({
_interactionSource: "form",
_formKey: "section1:line1:profileForm",
actions: {
submitForm: {
formId: "profileForm",
formKey: "section1:line1:profileForm",
formKey: "section1:line1:profile-contact-form",
actions: {
nextLine: {},
},
},
},
});
expect(cancelButton.click.payload.actions).toEqual({
cancelForm: {
formId: "profileForm",
formKey: "section1:line1:profileForm",
actions: {
rollbackByOffset: {},
},
},
});
expect(submitButton.click.payload.actions.submitForm).not.toHaveProperty(
"formId",
);
});

it("keeps edits transient until a valid multi-field submit commits variables and runs actions", () => {
Expand Down Expand Up @@ -260,8 +237,9 @@ describe("RouteEngine forms", () => {
expect(nameInput.value).toBe(" Ada ");
expect(emailInput.value).toBe("");
expect(
engine.selectSystemState().global.formDrafts["section1:line1:profileForm"]
.errors.email,
engine.selectSystemState().global.formDrafts[
"section1:line1:profile-contact-form"
].errors.email,
).toBe("required");

engine.handleActions(emailInput.change.payload.actions, {
Expand Down
17 changes: 16 additions & 1 deletion spec/createEffectsHandler.routeGraphicsEvents.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,6 @@ describe("createEffectsHandler RouteGraphics event bridge", () => {

await eventHandler("change", {
_interactionSource: "form",
_formKey: "section1:line1:profileForm",
actions: {
updateFormField: {
formKey: "section1:line1:profileForm",
Expand Down Expand Up @@ -374,6 +373,22 @@ describe("createEffectsHandler RouteGraphics event bridge", () => {
interactionSource: "form",
},
);

engine.handleActions.mockClear();

await eventHandler("click", {
_interactionSource: "form",
actions: {
submitForm: {
formKey: "section1:line9:profileForm",
actions: {
nextLine: {},
},
},
},
});

expect(engine.handleActions).not.toHaveBeenCalled();
});

it("coalesces replaceable effects by name and keeps the last payload", () => {
Expand Down
53 changes: 41 additions & 12 deletions src/createEffectsHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,12 @@ const createEffectsHandler = ({
return true;
};

const formInteractionActionTypes = new Set([
"updateFormField",
"submitForm",
"cancelForm",
]);

const getActiveInteraction = (engine) => {
if (typeof engine?.selectActiveInteraction === "function") {
return engine.selectActiveInteraction();
Expand All @@ -399,6 +405,12 @@ const createEffectsHandler = ({
return null;
};

const getFormInteractionKey = (value) => value?._formKey ?? value?.formKey;

const hasMatchingFormKey = (value, activeInteraction) => {
return getFormInteractionKey(value) === activeInteraction?.formKey;
};

const matchesInteraction = (value, activeInteraction) => {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
Expand All @@ -408,31 +420,48 @@ const createEffectsHandler = ({
return false;
}

if (
activeInteraction.source === "form" &&
value._formKey &&
value._formKey !== activeInteraction.formKey
) {
if (activeInteraction.source === "form") {
return hasMatchingFormKey(value, activeInteraction);
}

return true;
};

const matchesFormAction = (value, activeInteraction) => {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}

if (
activeInteraction.source === "form" &&
value.formKey &&
value.formKey !== activeInteraction.formKey
) {
if (activeInteraction?.source !== "form") {
return false;
}

return true;
return hasMatchingFormKey(value, activeInteraction);
};

const isInteractionPayload = (payload = {}, activeInteraction) => {
const actions = payload?.actions;

if (activeInteraction?.source === "form") {
if (matchesInteraction(payload, activeInteraction)) {
return true;
}

if (!actions || typeof actions !== "object" || Array.isArray(actions)) {
return false;
}

return Object.entries(actions).some(
([actionType, actionPayload]) =>
formInteractionActionTypes.has(actionType) &&
matchesFormAction(actionPayload, activeInteraction),
);
}

if (matchesInteraction(payload, activeInteraction)) {
return true;
}

const actions = payload?.actions;
if (!actions || typeof actions !== "object" || Array.isArray(actions)) {
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions src/schemas/presentationActions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -372,10 +372,10 @@ properties:
properties:
id:
type: string
description: Optional stable form ID. Defaults to resourceId.
description: Stable generated form ID. Defaults to resourceId for compatibility.
resourceId:
type: string
description: ID of the form layout UI element
description: ID of the form layout resource to render
fields:
type: object
description: Form fields keyed by layout field ID
Expand Down
6 changes: 0 additions & 6 deletions src/schemas/systemActions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,6 @@ properties:
type: object
description: Update a transient active form field draft
properties:
formId:
type: string
formKey:
type: string
field:
Expand All @@ -449,8 +447,6 @@ properties:
type: object
description: Submit the active form and run actions after a valid commit
properties:
formId:
type: string
formKey:
type: string
actions:
Expand All @@ -462,8 +458,6 @@ properties:
type: object
description: Cancel the active form and run follow-up actions
properties:
formId:
type: string
formKey:
type: string
actions:
Expand Down
Loading
Loading