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.3",
"version": "1.20.4",
"description": "A lightweight Visual Novel engine built in JavaScript for creating interactive narrative games with branching storylines",
"repository": {
"type": "git",
Expand Down
101 changes: 98 additions & 3 deletions spec/RouteEngine.form.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,29 @@ const createProjectData = () => ({
},
{
id: "submit-button",
type: "rect",
type: "container",
formRole: "submit",
x: 100,
y: 230,
width: 120,
height: 48,
children: [
{
id: "submit-label",
type: "rect",
x: 0,
y: 0,
width: 120,
height: 48,
click: {
payload: {
actions: {
nextLine: {},
},
},
},
},
],
},
],
},
Expand Down Expand Up @@ -116,7 +133,7 @@ const createProjectData = () => ({
},
});

const createEngine = () => {
const createEngine = ({ markLineCompleted = true } = {}) => {
const engine = createRouteEngine({
handlePendingEffects: () => {},
});
Expand All @@ -126,7 +143,10 @@ const createEngine = () => {
projectData: createProjectData(),
},
});
engine.handleAction("markLineCompleted", {});

if (markLineCompleted) {
engine.handleAction("markLineCompleted", {});
}

return engine;
};
Expand Down Expand Up @@ -207,6 +227,81 @@ describe("RouteEngine forms", () => {
expect(submitButton.click.payload.actions.submitForm).not.toHaveProperty(
"formId",
);
expect(Object.keys(submitButton.click.payload.actions)).toEqual([
"submitForm",
]);
});

it("submits when clicking a child inside a submit-role container", () => {
const engine = createEngine({ markLineCompleted: false });
let renderState = engine.selectRenderState();
const nameInput = findElement(renderState.elements, "name-input");
const emailInput = findElement(renderState.elements, "email-input");
let submitLabel = findElement(renderState.elements, "submit-label");

expect(submitLabel.click.payload).toMatchObject({
_interactionSource: "form",
actions: {
submitForm: {
formKey: "section1:line1:profile-contact-form",
actions: {
nextLine: {},
},
},
},
});

expect(Object.keys(submitLabel.click.payload.actions)).toEqual([
"submitForm",
]);

engine.handleActions(nameInput.change.payload.actions, {
_event: {
value: "Ada",
},
});
engine.handleActions(emailInput.change.payload.actions, {
_event: {
value: "ada@example.com",
},
});

renderState = engine.selectRenderState();
submitLabel = findElement(renderState.elements, "submit-label");
engine.handleActions(submitLabel.click.payload.actions);

expect(engine.selectSystemState().contexts[0].pointers.read.lineId).toBe(
"line2",
);
});

it("valid form submit advances even before the form line is marked completed", () => {
const engine = createEngine({ markLineCompleted: false });
let renderState = engine.selectRenderState();
const nameInput = findElement(renderState.elements, "name-input");
const emailInput = findElement(renderState.elements, "email-input");
let submitButton = findElement(renderState.elements, "submit-button");

expect(engine.selectSystemState().global.isLineCompleted).toBe(false);

engine.handleActions(nameInput.change.payload.actions, {
_event: {
value: "Ada",
},
});
engine.handleActions(emailInput.change.payload.actions, {
_event: {
value: "ada@example.com",
},
});

renderState = engine.selectRenderState();
submitButton = findElement(renderState.elements, "submit-button");
engine.handleActions(submitButton.click.payload.actions);

expect(engine.selectSystemState().contexts[0].pointers.read.lineId).toBe(
"line2",
);
});

it("keeps edits transient until a valid multi-field submit commits variables and runs actions", () => {
Expand Down
35 changes: 25 additions & 10 deletions src/stores/constructRenderState.js
Original file line number Diff line number Diff line change
Expand Up @@ -1542,7 +1542,7 @@ const FORM_EVENT_KEYS = new Set([

const FORM_ROLE_SUBMIT = "submit";

const mergeEventActions = (eventConfig, actions) => {
const mergeEventActions = (eventConfig, actions, options = {}) => {
const payload =
eventConfig?.payload &&
typeof eventConfig.payload === "object" &&
Expand All @@ -1553,31 +1553,44 @@ const mergeEventActions = (eventConfig, actions) => {
payload.actions && typeof payload.actions === "object"
? payload.actions
: {};
const nextActions =
options.replaceActions === true
? actions
: {
...existingActions,
...actions,
};

return {
...(eventConfig || {}),
payload: {
...payload,
actions: {
...existingActions,
...actions,
},
actions: nextActions,
},
};
};

const enrichFormElements = (node, form) => {
const enrichFormElements = (node, form, options = {}) => {
if (Array.isArray(node)) {
return node.map((item) => enrichFormElements(item, form));
return node.map((item) => enrichFormElements(item, form, options));
}

if (!node || typeof node !== "object") {
return node;
}

const isElementNode = typeof node.type === "string";
const isSubmitRole = isElementNode && node.formRole === FORM_ROLE_SUBMIT;
const inheritsSubmitRole = isElementNode && options.submitRoleAncestor;
const childOptions =
isSubmitRole || inheritsSubmitRole ? { submitRoleAncestor: true } : {};
const enrichedNode = {};
for (const [key, value] of Object.entries(node)) {
enrichedNode[key] = enrichFormElements(value, form);
enrichedNode[key] = enrichFormElements(
value,
form,
key === "children" ? childOptions : {},
);
}

let formNode = enrichedNode;
Expand Down Expand Up @@ -1614,10 +1627,12 @@ const enrichFormElements = (node, form) => {
}
}

if (formNode.formRole === FORM_ROLE_SUBMIT) {
if (formNode.formRole === FORM_ROLE_SUBMIT || inheritsSubmitRole) {
return {
...formNode,
click: mergeEventActions(formNode.click, form.submitActions),
click: mergeEventActions(formNode.click, form.submitActions, {
replaceActions: true,
}),
};
}

Expand Down
17 changes: 17 additions & 0 deletions src/stores/system.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -3420,6 +3420,22 @@ const createSubmittedFormContext = (activeForm, values) => ({
fieldList: cloneStateValue(activeForm.fieldList),
});

const completeCurrentLineForSubmittedForm = (state) => {
if (state.global.isLineCompleted) {
return;
}

state.global.isLineCompleted = true;
delete state.global.pendingScreenTransition;

const pointer = selectCurrentPointer({ state })?.pointer;
const sectionId = pointer?.sectionId;
const lineId = pointer?.lineId;
if (sectionId && lineId) {
recordViewedLine(state, { sectionId, lineId });
}
};

export const updateFormField = ({ state }, payload = {}) => {
const activeForm = getActiveFormForPayload(state, payload);
const fieldId = payload.field;
Expand Down Expand Up @@ -3546,6 +3562,7 @@ export const submitForm = ({ state }, payload = {}) => {
queueScopedDataPersistence(state, globalUpdates);

const formContext = createSubmittedFormContext(activeForm, storedValues);
completeCurrentLineForSubmittedForm(state);
delete state.global.formDrafts[activeForm.key];
state.global.pendingEffects.push({
name: "render",
Expand Down
33 changes: 21 additions & 12 deletions vt/specs/form/basic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ specs:
- background nextLine clicks are blocked while the form is visible
- empty submit keeps the form visible and shows required errors
- filled inputs stay visible before submit
- filling both inputs and clicking submit commits variables and advances
- filling both inputs and clicking submit child commits variables and advances
skipInitialScreenshot: true
viewport:
id: capture
Expand Down Expand Up @@ -138,21 +138,30 @@ resources:
content: "Code is required"
textStyleId: errorText
- id: submit-button
type: rect
type: container
formRole: submit
x: 650
y: 420
width: 190
height: 58
colorId: submitColor
click:
payload:
actions: ${form.submitActions}
- id: submit-label
type: text
x: 706
y: 436
content: "Submit"
textStyleId: buttonText
children:
- id: submit-hit
type: rect
x: 0
y: 0
width: 190
height: 58
colorId: submitColor
click:
payload:
actions:
nextLine: {}
- id: submit-label
type: text
x: 56
y: 16
content: "Submit"
textStyleId: buttonText
resultLayout:
elements:
- id: result-title
Expand Down
Loading