Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add typechecking to itemsExpression #3610

Closed
Closed
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
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@fortawesome/fontawesome-free": "^6.1.1",
"@open-formulieren/design-tokens": "^0.48.0",
"@open-formulieren/formio-builder": "^0.8.0",
"@open-formulieren/infernologic": "^0.1.0",
"@rjsf/core": "^4.2.1",
"@tinymce/tinymce-react": "^3.12.6",
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
Expand Down
69 changes: 69 additions & 0 deletions src/openforms/forms/tests/e2e_tests/test_form_designer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from openforms.utils.tests.cache import clear_caches
from openforms.variables.constants import FormVariableDataTypes, FormVariableSources

from ...models import Form
from ..factories import (
FormDefinitionFactory,
FormFactory,
Expand Down Expand Up @@ -2076,3 +2077,71 @@ def setUpTestData():
'duplicate-key: in "FORM DEFINITION #1" and "FORM DEFINITION #2"'
'duplicate-key-2: in "FORM DEFINITION #1" and "FORM DEFINITION #3"'
)


class FormDesignerJsonLogicTests(E2ETestCase):
async def test_variable_options(self) -> None:
@sync_to_async
def setUpTestData() -> Form:
return FormStepFactory.create(
form__name="Inferring type of variable options",
form_definition__name="Pick your poison",
form_definition__configuration={
"components": [
{"label": "Single choice", "type": "radio", "key": "single"},
{
"label": "Multiple choice",
"type": "selectboxes",
"key": "multi",
},
{"label": "Dropdown", "type": "select", "key": "dropdown"},
],
},
).form

await create_superuser()
form = await setUpTestData()

edit_form_url = str(
furl(self.live_server_url)
/ reverse("admin:forms_form_change", args=(form.pk,))
)

async def make_assertions():
await page.get_by_text("Keuzeopties", exact=True).click()
await page.get_by_text("Variabele", exact=True).click()
# clicking the "Items expression" label doesn't focus the editor :(
component = page.locator(
'css=[ref="component"]', has_text="Opties-expressie"
)
editor = component.locator("css=.ace_content")
await editor.click()
await editor.press("Control+a")
await editor.press("Backspace")
await editor.type('{"map": [{"var": "somevar"}]}')
await page.get_by_role("button", name="Opslaan").click()
await expect(page.locator("css=.formio-dialog-content")).to_be_visible()
await expect(
page.locator('css=[data-component-key="openForms.itemsExpression"]')
).to_contain_text("Error")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contents of "Error" will change. These assertions just assert that type check has occurred and accepts correct expressions.
Testing for the better error messages belongs in a unittest, E2E is way to slow and expensive for that.

await editor.click()
await editor.press("Control+a")
await editor.press("Backspace")
await editor.type(
'{"map": [{"var": "somevar"}, [{"var": ""}, {"cat": ["Optie ", {"var": ""}]}]]}'
)
Comment on lines +2130 to +2132
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it know that {"var": ""} in the context of {"var": "somevar"} (is this checked that it is of type array?) is a string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the exact order, but it follows from

  • {"cat": ["Optie ", {"var": ""}]}
  • the list of options is of string[][] / list[list[str]], therefore the result of the "map" is also of that type.

It's why the validateLogic passed the expression into

const result = infer({'===': [logic, expected]}, {});

with expected = [["", ""]] and logic = (in this case) the "map"

It uses the fact that "===" is a Callable[[T, T], boolean], so the parse of the result from the infer call is a lot easier. infer({"===": [{"var": "a"}, {"var": "b"}]}) does not imply that a and b have the same value, just the same type.

infer shouldn't return a string, of course, but before changing it, I like to have more use cases.

await page.get_by_role("button", name="Opslaan").click()
await expect(page.locator("css=.formio-dialog-content")).not_to_be_visible()

page: Page
async with browser_page() as page:
await self._admin_login(page)
await page.goto(edit_form_url)
await page.get_by_role("tab", name="Steps and fields").click()
await page.get_by_role("button", name="Pick your poison").click()
await open_component_options_modal(page, "Single choice")
await make_assertions()
await open_component_options_modal(page, "Multiple choice")
await make_assertions()
await open_component_options_modal(page, "Dropdown")
await make_assertions()
1 change: 1 addition & 0 deletions src/openforms/js/components/form/edit/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ const OPTIONS_CHOICES = [
},
validate: {
required: true,
custom: 'valid = (typeof input == "object") && validateLogic(input, [["", ""]])',
},
},
];
Expand Down
17 changes: 17 additions & 0 deletions src/openforms/js/components/formio_builder/builder.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {infer} from '@open-formulieren/infernologic';
import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
import PropTypes from 'prop-types';
Expand All @@ -17,6 +18,21 @@ const getBuilderOptions = () => {
const maxFileUploadSize = jsonScriptToVar('setting-MAX_FILE_UPLOAD_SIZE');
const formFieldsRequiredDefault = jsonScriptToVar('config-REQUIRED_DEFAULT');

/**
* For use in a FormIO custom validator.
*
*
* @param {object | array | number | string} logic - JsonLogic expression
* @param {object | array | number | string} expected - example expected result value [["label", "value"]]
* @returns {boolean | string} - OK or an error message
*/
const validateLogic = (logic, expected) => {
// We don't evaluate logic, we just look at types.
// "===" forces the logic expression align with the expectancy
const result = infer({'===': [logic, expected]}, {});
return result.startsWith('result type:') || result;
Comment on lines +32 to +33
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

infer API must change. This is ugly. Want to have more info from other tickets before making a decision and releasing a new npm package.

};

return {
builder: {
basic: false,
Expand Down Expand Up @@ -251,6 +267,7 @@ const getBuilderOptions = () => {
evalContext: {
serverUploadLimit: maxFileUploadSize,
requiredDefault: formFieldsRequiredDefault,
validateLogic,
},
editors: {
ckeditor: {
Expand Down
Loading