From a62dba4baf34eeca702541f5a30a869edf39ec19 Mon Sep 17 00:00:00 2001 From: Vladimir <90250006+Vlpros@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:16:14 +0300 Subject: [PATCH 1/8] Add stories for calculated expressions for boolean and choice * Boolean calculation * Add name props extFactory * Update boolean advence * Add Choise calculation tests * Rename data-test names and Boolean story * update choose func data-test * Update choice calculation * Clean code * Delete test-story book,revert quantity bug and add wait for cover * Delete debounce for select and add for quantity * Revert old package lock --- .../stories/assets/questionnaires/QBoolean.ts | 75 ---- .../stories/assets/questionnaires/QChoice.ts | 356 ------------------ .../itemTypes/AllCalculations.stories.tsx | 191 ++++++++++ .../src/stories/itemTypes/Boolean.stories.tsx | 7 - .../src/stories/itemTypes/Choice.stories.tsx | 19 +- .../src/stories/testUtils.ts | 29 +- packages/testing-toolkit/src/index.ts | 1 + 7 files changed, 220 insertions(+), 458 deletions(-) create mode 100644 packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx diff --git a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QBoolean.ts b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QBoolean.ts index 3d27dd5e4..1ff0aa4ff 100644 --- a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QBoolean.ts +++ b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QBoolean.ts @@ -103,78 +103,3 @@ export const qrBooleanCheckboxResponse: QuestionnaireResponse = { ], questionnaire: 'https://smartforms.csiro.au/docs/components/boolean/checkbox' }; - -export const qBooleanCalculation: Questionnaire = { - resourceType: 'Questionnaire', - id: 'BooleanCalculation', - name: 'BooleanCalculation', - title: 'Boolean Calculation', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/boolean/calculation', - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/variable', - valueExpression: { - name: 'gender', - language: 'text/fhirpath', - expression: "item.where(linkId = 'gender-controller').answer.valueCoding.code" - } - } - ], - item: [ - { - linkId: 'gender-controller', - text: 'Gender', - type: 'choice', - repeats: false, - answerOption: [ - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'female', - display: 'Female' - } - }, - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'male', - display: 'Male' - } - }, - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'other', - display: 'Other' - } - }, - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'unknown', - display: 'Unknown' - } - } - ] - }, - { - extension: [ - { - url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', - valueExpression: { - language: 'text/fhirpath', - expression: "%gender = 'female'" - } - } - ], - linkId: 'gender-is-female', - text: 'Gender is female?', - type: 'boolean', - readOnly: true - } - ] -}; diff --git a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QChoice.ts b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QChoice.ts index 7ce55921a..d1519e677 100644 --- a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QChoice.ts +++ b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QChoice.ts @@ -136,362 +136,6 @@ export const qrChoiceAnswerValueSetBasicResponse: QuestionnaireResponse = { questionnaire: 'https://smartforms.csiro.au/docs/components/choice/answervalueset-basic' }; -export const qChoiceAnswerOptionCalculation: Questionnaire = { - resourceType: 'Questionnaire', - id: 'ChoiceAnswerOptionCalculation', - name: 'ChoiceAnswerOptionCalculation', - title: 'Choice AnswerOption Calculation', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/choice/answeroption-calculation', - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/variable', - valueExpression: { - name: 'painLevel', - language: 'text/fhirpath', - expression: "item.where(linkId = 'pain-level').answer.value" - } - } - ], - item: [ - { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', - valueCodeableConcept: { - coding: [ - { - system: 'http://hl7.org/fhir/questionnaire-item-control', - code: 'slider' - } - ] - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-sliderStepValue', - valueInteger: 1 - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/minValue', - valueInteger: 0 - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/maxValue', - valueInteger: 10 - } - ], - linkId: 'pain-level', - text: 'Pain level', - type: 'integer', - repeats: false, - item: [ - { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', - valueCodeableConcept: { - coding: [ - { - system: 'http://hl7.org/fhir/questionnaire-item-control', - code: 'lower' - } - ] - } - } - ], - linkId: 'pain-level-lower', - text: 'No pain', - type: 'display' - }, - { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', - valueCodeableConcept: { - coding: [ - { - system: 'http://hl7.org/fhir/questionnaire-item-control', - code: 'upper' - } - ] - } - } - ], - linkId: 'pain-level-upper', - text: 'Unbearable pain', - type: 'display' - } - ] - }, - { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', - valueCodeableConcept: { - coding: [ - { - system: 'http://hl7.org/fhir/questionnaire-item-control', - code: 'radio-button' - } - ] - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation', - valueCode: 'horizontal' - }, - { - url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', - valueExpression: { - language: 'text/fhirpath', - expression: "iif(%painLevel.empty(), 'Y', iif(%painLevel < 5, 'Y', 'N'))" - } - } - ], - linkId: 'pain-low', - text: 'Low pain (Level < 5)', - type: 'choice', - repeats: false, - readOnly: true, - answerOption: [ - { - valueCoding: { - system: 'http://terminology.hl7.org/CodeSystem/v2-0532', - code: 'Y', - display: 'Yes' - } - }, - { - valueCoding: { - system: 'http://terminology.hl7.org/CodeSystem/v2-0532', - code: 'N', - display: 'No' - } - } - ] - } - ] -}; - -export const qChoiceAnswerValueSetCalculation: Questionnaire = { - resourceType: 'Questionnaire', - id: 'ChoiceAnswerValueSetCalculation', - name: 'ChoiceAnswerValueSetCalculation', - title: 'Choice AnswerValueSet Calculation', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/choice/answervalueset-calculation', - contained: [ - { - resourceType: 'ValueSet', - id: 'australian-states-territories-2', - meta: { - profile: [ - 'http://hl7.org/fhir/StructureDefinition/shareablevalueset', - 'https://healthterminologies.gov.au/fhir/StructureDefinition/composed-value-set-4' - ] - }, - url: 'https://healthterminologies.gov.au/fhir/ValueSet/australian-states-territories-2', - identifier: [ - { - system: 'urn:ietf:rfc:3986', - value: 'urn:oid:1.2.36.1.2001.1004.201.10026' - } - ], - version: '2.0.2', - name: 'AustralianStatesAndTerritories', - title: 'Australian States and Territories', - status: 'active', - experimental: false, - date: '2020-05-31', - publisher: 'Australian Digital Health Agency', - contact: [ - { - telecom: [ - { - system: 'email', - value: 'help@digitalhealth.gov.au' - } - ] - } - ], - description: - 'The Australian States and Territories value set includes values that represent the Australian states and territories.', - copyright: - 'Copyright © 2018 Australian Digital Health Agency - All rights reserved. Except for the material identified below, this content is licensed under a Creative Commons Attribution 4.0 International License. See https://creativecommons.org/licenses/by/4.0/. \n\nThis resource includes material that is based on Australian Institute of Health and Welfare material. \n\nAll copies of this resource must include this copyright statement and all information contained in this statement.', - compose: { - include: [ - { - system: - 'https://healthterminologies.gov.au/fhir/CodeSystem/australian-states-territories-1', - concept: [ - { - code: 'ACT' - }, - { - code: 'NSW' - }, - { - code: 'NT' - }, - { - code: 'OTHER' - }, - { - code: 'QLD' - }, - { - code: 'SA' - }, - { - code: 'TAS' - }, - { - code: 'VIC' - }, - { - code: 'WA' - } - ] - } - ] - }, - expansion: { - identifier: 'e9439195-c1d8-4069-a349-98c1d552a351', - timestamp: '2023-06-20T04:20:58+00:00', - total: 9, - offset: 0, - parameter: [ - { - name: 'version', - valueUri: - 'https://healthterminologies.gov.au/fhir/CodeSystem/australian-states-territories-1|1.1.3' - }, - { - name: 'count', - valueInteger: 2147483647 - }, - { - name: 'offset', - valueInteger: 0 - } - ], - contains: [ - { - system: - 'https://healthterminologies.gov.au/fhir/CodeSystem/australian-states-territories-1', - code: 'ACT', - display: 'Australian Capital Territory' - }, - { - system: - 'https://healthterminologies.gov.au/fhir/CodeSystem/australian-states-territories-1', - code: 'NSW', - display: 'New South Wales' - }, - { - system: - 'https://healthterminologies.gov.au/fhir/CodeSystem/australian-states-territories-1', - code: 'NT', - display: 'Northern Territory' - }, - { - system: - 'https://healthterminologies.gov.au/fhir/CodeSystem/australian-states-territories-1', - code: 'OTHER', - display: 'Other territories' - }, - { - system: - 'https://healthterminologies.gov.au/fhir/CodeSystem/australian-states-territories-1', - code: 'QLD', - display: 'Queensland' - }, - { - system: - 'https://healthterminologies.gov.au/fhir/CodeSystem/australian-states-territories-1', - code: 'SA', - display: 'South Australia' - }, - { - system: - 'https://healthterminologies.gov.au/fhir/CodeSystem/australian-states-territories-1', - code: 'TAS', - display: 'Tasmania' - }, - { - system: - 'https://healthterminologies.gov.au/fhir/CodeSystem/australian-states-territories-1', - code: 'VIC', - display: 'Victoria' - }, - { - system: - 'https://healthterminologies.gov.au/fhir/CodeSystem/australian-states-territories-1', - code: 'WA', - display: 'Western Australia' - } - ] - } - } - ], - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/variable', - valueExpression: { - name: 'stateCode', - language: 'text/fhirpath', - expression: "item.where(linkId = 'state-controller').answer.value" - } - } - ], - item: [ - { - linkId: 'state-controller-instructions', - text: 'Feel free to play around with the following state codes: ACT, NSW, NT, OTHER, QLD, SA, TAS, VIC, WA', - _text: { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/rendering-xhtml', - valueString: - '
\r\n
Feel free to play around with the following state codes:
\r\n \r\n
' - } - ] - }, - type: 'display', - repeats: false - }, - { - linkId: 'state-controller', - text: 'State (string)', - type: 'string', - repeats: false - }, - { - extension: [ - { - url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', - valueExpression: { - language: 'text/fhirpath', - expression: '%stateCode' - } - } - ], - linkId: 'state-choice', - text: 'State (choice)', - type: 'choice', - repeats: false, - readOnly: true, - answerValueSet: '#australian-states-territories-2' - } - ] -}; - export const qChoiceAnswerInitialSelected: Questionnaire = { resourceType: 'Questionnaire', id: 'TestChoiceSelectAnswerOptionsUsingInitialSelected', diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx new file mode 100644 index 000000000..29101feae --- /dev/null +++ b/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx @@ -0,0 +1,191 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; +import { + calculatedExpressionExtFactory, + getAnswers, + itemControlExtFactory, + questionnaireFactory, + variableExtFactory +} from '../testUtils'; +import { chooseSelectOption, inputText } from '@aehrc/testing-toolkit'; +import { expect, waitFor } from 'storybook/test'; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta = { + title: 'ItemType/CalculationScenario', + component: BuildFormWrapperForStorybook, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: [] +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const booleanTargetCoding = { + system: 'http://hl7.org/fhir/administrative-gender', + code: 'female', + display: 'Female' +}; +const booleanTargetlinkId = 'gender-controller'; +const booleanTargetlinkIdCalc = 'gender-is-female'; +const qBooleanCalculation = questionnaireFactory( + [ + { + linkId: booleanTargetlinkId, + text: 'Gender', + type: 'choice', + repeats: false, + answerOption: [ + { valueCoding: booleanTargetCoding }, + { + valueCoding: { + system: 'http://hl7.org/fhir/administrative-gender', + code: 'male', + display: 'Male' + } + } + ] + }, + { + extension: [calculatedExpressionExtFactory("%gender = 'female'")], + linkId: booleanTargetlinkIdCalc, + text: 'Gender is female?', + type: 'boolean', + readOnly: true + } + ], + { + extension: [ + variableExtFactory( + 'gender', + `item.where(linkId = '${booleanTargetlinkId}').answer.valueCoding.code` + ) + ] + } +); + +export const BooleanCalculation: Story = { + args: { + questionnaire: qBooleanCalculation + }, + play: async ({ canvasElement }) => { + await chooseSelectOption(canvasElement, booleanTargetlinkId, booleanTargetCoding.display); + + await waitFor(async () => { + const result = await getAnswers(booleanTargetlinkIdCalc); + expect(result).toHaveLength(1); + expect(result[0].valueBoolean).toBe(true); + }); + } +}; + +const choiceTargetLinkId = 'pain-level'; +const targetChoiceCoding = { + system: 'http://terminology.hl7.org/CodeSystem/v2-0532', + code: 'Y', + display: 'Yes' +}; +const choiceTargetLinkIdCalc = 'pain-low'; +const qChoiceAnswerOptionCalculation = questionnaireFactory( + [ + { + linkId: choiceTargetLinkId, + type: 'integer' + }, + { + extension: [ + itemControlExtFactory('radio-button'), + calculatedExpressionExtFactory( + "iif(%painLevel.empty(), 'Y', iif(%painLevel < 5, 'Y', 'N'))" + ) + ], + linkId: choiceTargetLinkIdCalc, + text: 'Low pain (Level < 5)', + type: 'choice', + readOnly: true, + answerOption: [ + { + valueCoding: targetChoiceCoding + }, + { + valueCoding: { + system: 'http://terminology.hl7.org/CodeSystem/v2-0532', + code: 'N', + display: 'No' + } + } + ] + } + ], + { + extension: [ + variableExtFactory('painLevel', `item.where(linkId = '${choiceTargetLinkId}').answer.value`) + ] + } +); +const choiceTargetNumber = 3; + +export const ChoiceAnswerOptionCalculation: Story = { + args: { + questionnaire: qChoiceAnswerOptionCalculation + }, + play: async ({ canvasElement }) => { + await inputText(canvasElement, choiceTargetLinkId, choiceTargetNumber); + + await waitFor(async () => { + const result = await getAnswers(choiceTargetLinkIdCalc); + expect(result).toHaveLength(1); + expect(result[0].valueCoding).toEqual(expect.objectContaining(targetChoiceCoding)); + }); + } +}; + +const choiceValueSetTargetLinkId = 'gender-string'; +const choiceValueSetTargetLinkIdCalc = 'gender-choice'; + +const choiceValueSetTargetCoding = { + system: 'http://hl7.org/fhir/administrative-gender', + code: 'male', + display: 'Male' +}; + +const qChoiceAnswerValueSetCalculation = questionnaireFactory( + [ + { + linkId: choiceValueSetTargetLinkId, + type: 'string', + text: 'Enter gender code' + }, + { + extension: [calculatedExpressionExtFactory('%gender')], + linkId: choiceValueSetTargetLinkIdCalc, + type: 'choice', + readOnly: true, + answerValueSet: 'http://hl7.org/fhir/ValueSet/administrative-gender' + } + ], + { + extension: [ + variableExtFactory( + 'gender', + `item.where((linkId = '${choiceValueSetTargetLinkId}')).answer.value` + ) + ] + } +); + +export const ChoiceAnswerValueSetCalculation: Story = { + args: { + questionnaire: qChoiceAnswerValueSetCalculation + }, + play: async ({ canvasElement }) => { + await inputText(canvasElement, choiceValueSetTargetLinkId, choiceValueSetTargetCoding.code); + + await waitFor(async () => { + const result = await getAnswers(choiceValueSetTargetLinkIdCalc); + expect(result).toHaveLength(1); + expect(result[0].valueCoding).toEqual(expect.objectContaining(choiceValueSetTargetCoding)); + }); + } +}; diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Boolean.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Boolean.stories.tsx index 43aefa9b8..7a85e6335 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Boolean.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Boolean.stories.tsx @@ -16,7 +16,6 @@ */ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { qBooleanCalculation } from '../assets/questionnaires'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export import { getAnswers, itemControlExtFactory, qrFactory, questionnaireFactory } from '../testUtils'; import { @@ -160,9 +159,3 @@ export const BooleanCheckboxResponse: Story = { expect(input).toBeChecked(); } }; - -export const BooleanCalculation: Story = { - args: { - questionnaire: qBooleanCalculation - } -}; diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Choice.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Choice.stories.tsx index eb6d36e4f..c99d5a980 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Choice.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Choice.stories.tsx @@ -17,10 +17,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { - qChoiceAnswerOptionCalculation, - qChoiceAnswerValueSetCalculation -} from '../assets/questionnaires'; + import { chooseSelectOption, findByLinkId, getInputText } from '@aehrc/testing-toolkit'; import { getAnswers, qrFactory, questionnaireFactory } from '../testUtils'; import { expect, fireEvent } from 'storybook/test'; @@ -199,17 +196,3 @@ export const ChoiceAnswerOptionsUsingInitialSelected: Story = { expect(inputText).toBe(initialTargetCoding.display); } }; - -// TODO: Move to separate storybook -export const ChoiceAnswerOptionCalculation: Story = { - args: { - questionnaire: qChoiceAnswerOptionCalculation - } -}; - -// TODO: Move to separate storybook -export const ChoiceAnswerValueSetCalculation: Story = { - args: { - questionnaire: qChoiceAnswerValueSetCalculation - } -}; diff --git a/packages/smart-forms-renderer/src/stories/testUtils.ts b/packages/smart-forms-renderer/src/stories/testUtils.ts index 6ee21b4bf..cece61d63 100644 --- a/packages/smart-forms-renderer/src/stories/testUtils.ts +++ b/packages/smart-forms-renderer/src/stories/testUtils.ts @@ -26,11 +26,15 @@ export async function getGroupAnswers(groupLinkid: string, answerLinkid: string) return result; } -export function questionnaireFactory(items: QuestionnaireItem[]): Questionnaire { +export function questionnaireFactory( + items: QuestionnaireItem[], + extra?: Omit +): Questionnaire { return { resourceType: 'Questionnaire', status: 'active', - item: items + item: items, + ...extra }; } @@ -60,3 +64,24 @@ export function openLabelExtFactory(text: string): Extension { valueString: text }; } + +export function calculatedExpressionExtFactory(text: string): Extension { + return { + url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', + valueExpression: { + language: 'text/fhirpath', + expression: text + } + }; +} + +export function variableExtFactory(name: string, text: string): Extension { + return { + url: 'http://hl7.org/fhir/StructureDefinition/variable', + valueExpression: { + name: name, + language: 'text/fhirpath', + expression: text + } + }; +} diff --git a/packages/testing-toolkit/src/index.ts b/packages/testing-toolkit/src/index.ts index adf1f8078..839db2772 100644 --- a/packages/testing-toolkit/src/index.ts +++ b/packages/testing-toolkit/src/index.ts @@ -209,6 +209,7 @@ export async function chooseQuantityOption( } fireEvent.change(inputWeight, { target: { value: quantity } }); + // Here we await for debounced store update await new Promise((resolve) => setTimeout(resolve, 500)); } From b93a8bc0cc67d44483e8f425271213f8499e0217 Mon Sep 17 00:00:00 2001 From: Vladimir <90250006+Vlpros@users.noreply.github.com> Date: Sat, 13 Sep 2025 18:29:20 +0300 Subject: [PATCH 2/8] Add decimal and display calculations --- .../questionnaires/QBehaviorCalculations.ts | 99 ------------------- .../stories/assets/questionnaires/QDisplay.ts | 77 --------------- .../itemTypes/AllCalculations.stories.tsx | 58 ++++++++++- .../itemTypes/AllCqfExpressions.stories.tsx | 88 +++++++++++++++++ .../src/stories/itemTypes/Decimal.stories.tsx | 8 +- .../src/stories/itemTypes/Display.stories.tsx | 8 +- .../src/stories/testUtils.ts | 10 ++ 7 files changed, 157 insertions(+), 191 deletions(-) create mode 100644 packages/smart-forms-renderer/src/stories/itemTypes/AllCqfExpressions.stories.tsx diff --git a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QBehaviorCalculations.ts b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QBehaviorCalculations.ts index eb887559c..272722697 100644 --- a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QBehaviorCalculations.ts +++ b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QBehaviorCalculations.ts @@ -104,105 +104,6 @@ export const qInitialExpression: Questionnaire = { ] }; -export const qCalculatedExpressionBMICalculator: Questionnaire = { - resourceType: 'Questionnaire', - id: 'CalculatedExpressionBMICalculator', - name: 'CalculatedExpressionBMICalculator', - title: 'Calculated Expression BMI Calculator', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/behavior/choice-restrictions/calculated-expression-1', - item: [ - { - linkId: 'bmi-calculation', - text: 'BMI Calculation', - type: 'group', - repeats: false, - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/variable', - valueExpression: { - name: 'height', - language: 'text/fhirpath', - expression: "item.where(linkId='patient-height').answer.value" - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/variable', - valueExpression: { - name: 'weight', - language: 'text/fhirpath', - expression: "item.where(linkId='patient-weight').answer.value" - } - } - ], - item: [ - { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'cm', - display: 'cm' - } - } - ], - linkId: 'patient-height', - text: 'Height', - type: 'decimal', - repeats: false, - readOnly: false - }, - { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'kg', - display: 'kg' - } - } - ], - linkId: 'patient-weight', - text: 'Weight', - type: 'decimal', - repeats: false, - readOnly: false - }, - { - extension: [ - { - url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', - valueExpression: { - description: 'BMI calculation', - language: 'text/fhirpath', - expression: '(%weight/((%height/100).power(2))).round(1)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'kg/m2', - display: 'kg/m2' - } - } - ], - linkId: 'bmi-result', - text: 'Value', - type: 'decimal', - repeats: false, - readOnly: true - } - ] - } - ] -}; - export const qCalculatedExpressionCvdRiskCalculator: Questionnaire = { resourceType: 'Questionnaire', id: 'CalculatedExpressionCvdRiskCalculator', diff --git a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QDisplay.ts b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QDisplay.ts index 64f8fb9c2..cbfa924ef 100644 --- a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QDisplay.ts +++ b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QDisplay.ts @@ -37,83 +37,6 @@ export const qDisplayBasic: Questionnaire = { ] }; -export const qDisplayCalculation: Questionnaire = { - resourceType: 'Questionnaire', - id: 'DisplayCalculation', - name: 'DisplayCalculation', - title: 'Display Calculation', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/display/calculation', - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/variable', - valueExpression: { - name: 'gender', - language: 'text/fhirpath', - expression: "item.where(linkId = 'gender-controller').answer.valueCoding.code" - } - } - ], - item: [ - { - linkId: 'gender-controller', - text: 'Gender', - type: 'choice', - repeats: false, - answerOption: [ - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'female', - display: 'Female' - } - }, - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'male', - display: 'Male' - } - }, - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'other', - display: 'Other' - } - }, - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'unknown', - display: 'Unknown' - } - } - ] - }, - { - linkId: 'gender-display', - type: 'display', - repeats: false, - _text: { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/cqf-expression', - valueExpression: { - language: 'text/fhirpath', - expression: "'Gender: '+ %gender" - } - } - ] - }, - text: '' - } - ] -}; - export const qDisplayCalculationStyled: Questionnaire = { resourceType: 'Questionnaire', id: 'DisplayCalculationStyled', diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx index 29101feae..84a1ed395 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx @@ -4,11 +4,12 @@ import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperF import { calculatedExpressionExtFactory, getAnswers, + getGroupAnswers, itemControlExtFactory, questionnaireFactory, variableExtFactory } from '../testUtils'; -import { chooseSelectOption, inputText } from '@aehrc/testing-toolkit'; +import { chooseSelectOption, inputDecimal, inputText } from '@aehrc/testing-toolkit'; import { expect, waitFor } from 'storybook/test'; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export @@ -189,3 +190,58 @@ export const ChoiceAnswerValueSetCalculation: Story = { }); } }; +const heightLinkId = 'patient-height'; +const weightLinkId = 'patient-weight'; +const bmiLinkIdCalc = 'bmi-result'; +const bmiGroupLinkId = 'bmi-calculation'; + +const qCalculatedExpressionBMICalculator = questionnaireFactory([ + { + linkId: bmiGroupLinkId, + type: 'group', + extension: [ + variableExtFactory('height', `item.where(linkId='${heightLinkId}').answer.value`), + variableExtFactory('weight', `item.where(linkId='${weightLinkId}').answer.value`) + ], + item: [ + { + linkId: heightLinkId, + text: 'Height', + type: 'decimal', + readOnly: false + }, + { + linkId: weightLinkId, + text: 'Weight', + type: 'decimal', + readOnly: false + }, + { + extension: [calculatedExpressionExtFactory('(%weight/((%height/100).power(2))).round(1)')], + linkId: bmiLinkIdCalc, + text: 'Value', + type: 'decimal', + readOnly: true + } + ] + } +]); +const heightTarget = 100; +const weightTarget = 10; +const bmiResultCalc = 10; + +export const DecimalCalculation: Story = { + args: { + questionnaire: qCalculatedExpressionBMICalculator + }, + play: async ({ canvasElement }) => { + await inputDecimal(canvasElement, heightLinkId, heightTarget); + await inputDecimal(canvasElement, weightLinkId, weightTarget); + + await waitFor(async () => { + const result = await getGroupAnswers(bmiGroupLinkId, bmiLinkIdCalc); + expect(result).toHaveLength(1); + expect(result[0].valueDecimal).toBe(bmiResultCalc); + }); + } +}; diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/AllCqfExpressions.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/AllCqfExpressions.stories.tsx new file mode 100644 index 000000000..666a5dbe6 --- /dev/null +++ b/packages/smart-forms-renderer/src/stories/itemTypes/AllCqfExpressions.stories.tsx @@ -0,0 +1,88 @@ +/* + * Copyright 2025 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { expect, within } from 'storybook/test'; +import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; +import { chooseSelectOption } from '@aehrc/testing-toolkit'; +import { questionnaireFactory, variableExtFactory, сqfExpressionFactory } from '../testUtils'; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta = { + title: 'ItemType/CqfExpressionScenario', + component: BuildFormWrapperForStorybook, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: [] +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const displayTargetLinkId = 'gender-controller'; +const genderTargetCoding = { + system: 'http://hl7.org/fhir/administrative-gender', + code: 'Female', + display: 'Female' +}; +const qDisplayCalculation = questionnaireFactory( + [ + { + linkId: displayTargetLinkId, + type: 'choice', + answerOption: [ + { + valueCoding: genderTargetCoding + }, + { + valueCoding: { + system: 'http://hl7.org/fhir/administrative-gender', + code: 'Male', + display: 'Male' + } + } + ] + }, + { + linkId: 'gender-display', + type: 'display', + _text: { + extension: [сqfExpressionFactory("'Gender: '+ %gender")] + } + } + ], + { + extension: [ + variableExtFactory( + 'gender', + `item.where(linkId = '${displayTargetLinkId}').answer.valueCoding.code` + ) + ] + } +); +const displayTargetText = 'Gender: ' + genderTargetCoding.display; + +export const DisplayCalculation: Story = { + args: { + questionnaire: qDisplayCalculation + }, + play: async ({ canvasElement }) => { + await chooseSelectOption(canvasElement, displayTargetLinkId, genderTargetCoding.display); + + const element = within(canvasElement); + expect(element.queryAllByText(displayTargetText)).toBeDefined(); + } +}; diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Decimal.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Decimal.stories.tsx index 39b9dbbd0..8661583e9 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Decimal.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Decimal.stories.tsx @@ -17,7 +17,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { qCalculatedExpressionBMICalculator } from '../assets/questionnaires'; + import { getAnswers, qrFactory, questionnaireFactory } from '../testUtils'; import { findByLinkId, getInputText, inputDecimal } from '@aehrc/testing-toolkit'; import { expect, fireEvent } from 'storybook/test'; @@ -95,9 +95,3 @@ export const DecimalBasicResponse: Story = { expect(input).toBe(targetWeight.toString()); } }; - -export const DecimalCalculation: Story = { - args: { - questionnaire: qCalculatedExpressionBMICalculator - } -}; diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Display.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Display.stories.tsx index b0e7920d9..58e21d910 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Display.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Display.stories.tsx @@ -17,7 +17,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { qDisplayCalculation, qDisplayCalculationStyled } from '../assets/questionnaires/QDisplay'; +import { qDisplayCalculationStyled } from '../assets/questionnaires/QDisplay'; import { questionnaireFactory } from '../testUtils'; import { expect, screen } from 'storybook/test'; @@ -54,12 +54,6 @@ export const DisplayBasic: Story = { } }; -export const DisplayCalculation: Story = { - args: { - questionnaire: qDisplayCalculation - } -}; - export const DisplayCalculationStyled: Story = { args: { questionnaire: qDisplayCalculationStyled diff --git a/packages/smart-forms-renderer/src/stories/testUtils.ts b/packages/smart-forms-renderer/src/stories/testUtils.ts index cece61d63..b959fd1cd 100644 --- a/packages/smart-forms-renderer/src/stories/testUtils.ts +++ b/packages/smart-forms-renderer/src/stories/testUtils.ts @@ -85,3 +85,13 @@ export function variableExtFactory(name: string, text: string): Extension { } }; } + +export function сqfExpressionFactory(text: string) { + return { + url: 'http://hl7.org/fhir/StructureDefinition/cqf-expression', + valueExpression: { + language: 'text/fhirpath', + expression: text + } + }; +} From 4c3e8af58a7875b0aa6c68b52d247040c9fe110d Mon Sep 17 00:00:00 2001 From: Vladimir <90250006+Vlpros@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:22:53 +0300 Subject: [PATCH 3/8] Add tests for string and text calculation * Add string calculation * Add text calculation * Delete group --- .../stories/assets/questionnaires/QString.ts | 131 -------------- .../stories/assets/questionnaires/QText.ts | 169 ------------------ .../itemTypes/AllCalculations.stories.tsx | 100 +++++++++++ .../src/stories/itemTypes/String.stories.tsx | 7 - .../src/stories/itemTypes/Text.stories.tsx | 8 - 5 files changed, 100 insertions(+), 315 deletions(-) delete mode 100644 packages/smart-forms-renderer/src/stories/assets/questionnaires/QString.ts delete mode 100644 packages/smart-forms-renderer/src/stories/assets/questionnaires/QText.ts diff --git a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QString.ts b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QString.ts deleted file mode 100644 index 3eef94cd1..000000000 --- a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QString.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2025 Commonwealth Scientific and Industrial Research - * Organisation (CSIRO) ABN 41 687 119 230. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Questionnaire } from 'fhir/r4'; -import type { QuestionnaireResponse } from 'fhir/r4'; - -export const qStringBasic: Questionnaire = { - resourceType: 'Questionnaire', - id: 'StringBasic', - name: 'StringBasic', - title: 'String Basic', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/string/basic', - item: [ - { - linkId: 'name', - type: 'string', - repeats: false, - text: 'Name' - } - ] -}; - -export const qrStringBasicResponse: QuestionnaireResponse = { - resourceType: 'QuestionnaireResponse', - status: 'in-progress', - item: [ - { - linkId: 'name', - text: 'Name', - answer: [ - { - valueString: 'John Doe' - } - ] - } - ], - questionnaire: 'https://smartforms.csiro.au/docs/components/string/basic' -}; - -export const qStringCalculation: Questionnaire = { - resourceType: 'Questionnaire', - id: 'StringCalculation', - name: 'StringCalculation', - title: 'String Calculation', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/string/calculation', - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/variable', - valueExpression: { - name: 'gender', - language: 'text/fhirpath', - expression: "item.where(linkId = 'gender-controller').answer.valueCoding.code" - } - } - ], - item: [ - { - linkId: 'gender-controller', - text: 'Gender', - type: 'choice', - repeats: false, - answerOption: [ - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'female', - display: 'Female' - } - }, - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'male', - display: 'Male' - } - }, - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'other', - display: 'Other' - } - }, - { - valueCoding: { - system: 'http://hl7.org/fhir/administrative-gender', - code: 'unknown', - display: 'Unknown' - } - } - ] - }, - { - extension: [ - { - url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', - valueExpression: { - language: 'text/fhirpath', - expression: '%gender' - } - } - ], - linkId: 'gender-string', - text: 'Gender code', - type: 'string', - readOnly: true - } - ] -}; diff --git a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QText.ts b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QText.ts deleted file mode 100644 index 40af2544b..000000000 --- a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QText.ts +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2025 Commonwealth Scientific and Industrial Research - * Organisation (CSIRO) ABN 41 687 119 230. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Questionnaire } from 'fhir/r4'; -import type { QuestionnaireResponse } from 'fhir/r4'; - -export const qTextBasic: Questionnaire = { - resourceType: 'Questionnaire', - id: 'TextBasic', - name: 'TextBasic', - title: 'Text Basic', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/text/basic', - item: [ - { - linkId: 'details', - type: 'text', - repeats: false, - text: 'Details of intermittent fasting' - } - ] -}; - -export const qrTextBasicResponse: QuestionnaireResponse = { - resourceType: 'QuestionnaireResponse', - status: 'in-progress', - item: [ - { - linkId: 'details', - text: 'Details of intermittent fasting', - answer: [ - { - valueString: - '- 8 hour eating window\n- Cup of black coffee in the morning\n- Small portions of lunch and dinner' - } - ] - } - ], - questionnaire: 'https://smartforms.csiro.au/docs/components/text/basic' -}; - -export const qTextCalculation: Questionnaire = { - resourceType: 'Questionnaire', - id: 'TextCalculation', - name: 'TextCalculation', - title: 'Text Calculation', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/text/calculation', - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/variable', - valueExpression: { - name: 'earHealthDetails', - language: 'text/fhirpath', - expression: - "item.where(linkId = 'ear-health-group').item.where(linkId = 'ear-health-details').answer.value" - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/variable', - valueExpression: { - name: 'medicationDetails', - language: 'text/fhirpath', - expression: - "item.where(linkId = 'medications-group').item.where(linkId = 'medications-details').answer.value" - } - } - ], - item: [ - { - linkId: 'ear-health-group', - text: 'Ear Health', - type: 'group', - repeats: false, - item: [ - { - linkId: 'ear-health-other', - text: '... (other questions)', - type: 'display', - readOnly: false - }, - { - linkId: 'ear-health-details', - text: 'Details', - type: 'text', - readOnly: false - } - ] - }, - { - linkId: 'medications-group', - text: 'Medications', - type: 'group', - repeats: false, - item: [ - { - linkId: 'medications-other', - text: '... (other questions)', - type: 'display', - readOnly: false - }, - { - linkId: 'medications-details', - text: 'Details', - type: 'text', - readOnly: false - } - ] - }, - { - linkId: 'summaries-group', - text: 'Health Check Summaries', - type: 'group', - repeats: false, - item: [ - { - extension: [ - { - url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', - valueExpression: { - language: 'text/fhirpath', - expression: '%earHealthDetails' - } - } - ], - linkId: 'ear-health-summary', - text: 'Ear Health', - type: 'text', - readOnly: true - }, - { - extension: [ - { - url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', - valueExpression: { - language: 'text/fhirpath', - expression: '%medicationDetails' - } - } - ], - linkId: 'medication-summary', - text: 'Medications', - type: 'text', - readOnly: true - } - ] - } - ] -}; diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx index 84a1ed395..d6b737fe7 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx @@ -245,3 +245,103 @@ export const DecimalCalculation: Story = { }); } }; + +const stringCalculationLinkId = 'gender-controller'; +const stringCalcCalculationLinkId = 'gender-string'; +const stringTargetCoding = { + system: 'http://hl7.org/fhir/administrative-gender', + code: 'Male', + display: 'Male' +}; +const qStringCalculation = questionnaireFactory( + [ + { + linkId: stringCalculationLinkId, + type: 'choice', + answerOption: [ + { + valueCoding: { + system: 'http://hl7.org/fhir/administrative-gender', + code: 'Female', + display: 'Female' + } + }, + { + valueCoding: stringTargetCoding + } + ] + }, + { + extension: [calculatedExpressionExtFactory('%gender')], + linkId: stringCalcCalculationLinkId, + type: 'string', + readOnly: true + } + ], + { + extension: [ + variableExtFactory( + 'gender', + `item.where(linkId = '${stringCalculationLinkId}').answer.valueCoding.code` + ) + ] + } +); + +export const StringCalculation: Story = { + args: { + questionnaire: qStringCalculation + }, + play: async ({ canvasElement }) => { + await chooseSelectOption(canvasElement, stringCalculationLinkId, stringTargetCoding.display); + + await waitFor(async () => { + const result = await getAnswers(stringCalcCalculationLinkId); + expect(result).toHaveLength(1); + expect(result[0].valueString).toBe(stringTargetCoding.display); + }); + } +}; + +const detailsLinkId = 'medications-details'; +const detailsCalcLinkId = 'medication-summary'; +const textTargetText = 'Hello world'; + +const qTextCalculation = questionnaireFactory( + [ + { + linkId: detailsLinkId, + type: 'text', + readOnly: false + }, + { + extension: [calculatedExpressionExtFactory('%medicationDetails')], + linkId: detailsCalcLinkId, + type: 'text', + readOnly: true + } + ], + { + extension: [ + variableExtFactory( + 'medicationDetails', + `item.where((linkId = '${detailsLinkId}')).answer.value` + ) + ] + } +); + +export const TextCalculation: Story = { + args: { + questionnaire: qTextCalculation + }, + play: async ({ canvasElement }) => { + await inputText(canvasElement, detailsLinkId, textTargetText); + + await waitFor(async () => { + const result = await getAnswers(detailsCalcLinkId); + expect(result).toHaveLength(1); + expect(result[0].valueString).toBe(textTargetText); + }); + } +}; diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/String.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/String.stories.tsx index 6e5921b36..a55d4dd4b 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/String.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/String.stories.tsx @@ -17,7 +17,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { qStringCalculation } from '../assets/questionnaires'; import { getAnswers, qrFactory, questionnaireFactory } from '../testUtils'; import { findByLinkId, getInputText, inputText } from '@aehrc/testing-toolkit'; import { expect, fireEvent } from 'storybook/test'; @@ -92,9 +91,3 @@ export const StringBasicResponse: Story = { expect(inputText).toBe(targetText); } }; - -export const StringCalculation: Story = { - args: { - questionnaire: qStringCalculation - } -}; diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Text.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Text.stories.tsx index 19cd9fe00..f376f0db0 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Text.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Text.stories.tsx @@ -17,7 +17,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { qTextCalculation } from '../assets/questionnaires'; import { inputText, getInputText, findByLinkId } from '@aehrc/testing-toolkit'; import { expect, fireEvent } from 'storybook/test'; import { getAnswers, qrFactory, questionnaireFactory } from '../testUtils'; @@ -79,10 +78,3 @@ export const TextBasicResponse: Story = { expect(inputText).toBe(targetText); } }; - -// TODO: Move to separate storybook -export const TextCalculation: Story = { - args: { - questionnaire: qTextCalculation - } -}; From b9b2bc11d3fd51df5047f701c810a0395cd5f2ab Mon Sep 17 00:00:00 2001 From: Vladimir <90250006+Vlpros@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:56:39 +0300 Subject: [PATCH 4/8] Add integer and quantity calculation (#18) * Add integer calc * Add integer * Fix error bsections and and quantity funcs * Delete qunatity from assets * Add quantity section tests * Update quantity calculation * Update testUtils for quantity and qunatity formComponents data-test * Delete Qinteger from assets * Delete unitFactory * Integer clean code * Clean code toolkit * Clean code quantity stories * Quantity section * Fix quantity and all calcs * Add system * Update single unit test --- .../QuantityItem/QuantityUnitField.tsx | 7 +- .../stories/assets/questionnaires/QInteger.ts | 119 ------ .../assets/questionnaires/QQuantity.ts | 365 ------------------ .../stories/assets/questionnaires/index.ts | 5 +- .../itemTypes/AllCalculations.stories.tsx | 94 ++++- .../src/stories/itemTypes/Integer.stories.tsx | 7 - .../stories/itemTypes/Quantity.stories.tsx | 361 ++++++++++++++++- .../src/stories/testUtils.ts | 23 ++ packages/testing-toolkit/src/index.ts | 127 +++++- 9 files changed, 582 insertions(+), 526 deletions(-) delete mode 100644 packages/smart-forms-renderer/src/stories/assets/questionnaires/QInteger.ts delete mode 100644 packages/smart-forms-renderer/src/stories/assets/questionnaires/QQuantity.ts diff --git a/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityUnitField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityUnitField.tsx index b8e9b0577..ece4da84b 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityUnitField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityUnitField.tsx @@ -55,7 +55,12 @@ function QuantityUnitField(props: QuantityUnitFieldProps) { readOnly={readOnly && readOnlyVisualStyle === 'readonly'} size="small" renderInput={(params) => ( - + )} /> ); diff --git a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QInteger.ts b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QInteger.ts deleted file mode 100644 index 7802db3e5..000000000 --- a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QInteger.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2025 Commonwealth Scientific and Industrial Research - * Organisation (CSIRO) ABN 41 687 119 230. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Questionnaire } from 'fhir/r4'; -import type { QuestionnaireResponse } from 'fhir/r4'; - -export const qIntegerBasic: Questionnaire = { - resourceType: 'Questionnaire', - id: 'IntegerBasic', - name: 'IntegerBasic', - title: 'Integer Basic', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/integer/basic', - item: [ - { - linkId: 'age', - type: 'integer', - repeats: false, - text: 'Age' - } - ] -}; - -export const qrIntegerBasicResponse: QuestionnaireResponse = { - resourceType: 'QuestionnaireResponse', - status: 'in-progress', - item: [ - { - linkId: 'age', - text: 'Age', - answer: [ - { - valueInteger: 40 - } - ] - } - ], - questionnaire: 'https://smartforms.csiro.au/docs/components/integer/basic' -}; - -export const qIntegerCalculation: Questionnaire = { - resourceType: 'Questionnaire', - id: 'IntegerCalculation', - name: 'IntegerCalculation', - title: 'Integer Calculation', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/integer/calculation', - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/variable', - valueExpression: { - name: 'length', - language: 'text/fhirpath', - expression: "item.where(linkId = 'length-controller').answer.value" - } - } - ], - item: [ - { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'cm', - display: 'cm' - } - } - ], - linkId: 'length-controller', - text: 'Length (cm)', - type: 'integer', - repeats: false - }, - { - extension: [ - { - url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', - valueExpression: { - language: 'text/fhirpath', - expression: '%length.power(2)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'cm2', - display: 'cm^2' - } - } - ], - linkId: 'length-squared', - text: 'Length squared (cm^2)', - type: 'integer', - readOnly: true - } - ] -}; diff --git a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QQuantity.ts b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QQuantity.ts deleted file mode 100644 index 4aee9ae8e..000000000 --- a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QQuantity.ts +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Copyright 2025 Commonwealth Scientific and Industrial Research - * Organisation (CSIRO) ABN 41 687 119 230. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; - -export const qQuantityBasic: Questionnaire = { - resourceType: 'Questionnaire', - id: 'QuantityBasic', - name: 'QuantityBasic', - title: 'Quantity Basic', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/quantity/basic', - item: [ - { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit', - valueCoding: { system: 'http://unitsofmeasure.org', code: 'kg', display: 'kg' } - } - ], - linkId: 'body-weight', - type: 'quantity', - repeats: false, - text: 'Body Weight' - }, - { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit', - valueCoding: { system: 'http://unitsofmeasure.org', code: 'kg', display: 'kg' } - } - ], - linkId: 'body-weight-comparator', - type: 'quantity', - repeats: false, - text: 'Body Weight (with comparator symbol)' - } - ] -}; - -export const qrQuantityBasicResponse: QuestionnaireResponse = { - resourceType: 'QuestionnaireResponse', - status: 'in-progress', - item: [ - { - linkId: 'body-weight', - answer: [ - { - valueQuantity: { - value: 80, - unit: 'kg', - system: 'http://unitsofmeasure.org', - code: 'kg' - } - } - ], - text: 'Body Weight' - }, - { - linkId: 'body-weight-comparator', - answer: [ - { - valueQuantity: { - value: 90, - comparator: '<', - unit: 'kg', - system: 'http://unitsofmeasure.org', - code: 'kg' - } - } - ], - text: 'Body Weight (with comparator symbol)' - } - ], - questionnaire: 'https://smartforms.csiro.au/docs/components/quantity/basic' -}; - -export const qQuantityUnitOption: Questionnaire = { - resourceType: 'Questionnaire', - id: 'QuantityUnitOption', - name: 'QuantityUnitOption', - title: 'Quantity UnitOption', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-07-27', - url: 'https://smartforms.csiro.au/docs/components/quantity/unit-option', - item: [ - { - linkId: 'duration-single-unit-guidance', - text: 'If there is only one unitOption, it will be rendered as if a "http://hl7.org/fhir/StructureDefinition/questionnaire-unit" extension is used.', - type: 'display' - }, - { - linkId: 'duration-single-unit', - text: 'Duration (single unit)', - type: 'quantity', - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'd', - display: 'Day(s)' - } - } - ] - }, - { - linkId: 'duration-multi-unit-guidance', - text: 'If there are multiple unitOptions, they will be rendered in a separate dropdown field.', - type: 'display' - }, - { - linkId: 'duration-multi-unit', - text: 'Duration (multiple units)', - type: 'quantity', - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'd', - display: 'Day(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'wk', - display: 'Week(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'mo', - display: 'Month(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'a', - display: 'Year(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 's', - display: 'Second(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'min', - display: 'Minute(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'hour', - display: 'Hour(s)' - } - } - ] - }, - { - linkId: 'duration-multi-unit-comparator', - text: 'Duration (multiple units, with comparator symbol)', - type: 'quantity', - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'd', - display: 'Day(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'wk', - display: 'Week(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'mo', - display: 'Month(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'a', - display: 'Year(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 's', - display: 'Second(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'min', - display: 'Minute(s)' - } - }, - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', - valueCoding: { - system: 'http://unitsofmeasure.org', - code: 'hour', - display: 'Hour(s)' - } - } - ] - } - ] -}; - -export const qrQuantityUnitOptionResponse: QuestionnaireResponse = { - resourceType: 'QuestionnaireResponse', - status: 'in-progress', - item: [ - { - linkId: 'duration-single-unit', - answer: [ - { - valueQuantity: { - value: 2, - unit: 'Day(s)', - system: 'http://unitsofmeasure.org', - code: 'd' - } - } - ], - text: 'Duration' - }, - { - linkId: 'duration-multi-unit', - answer: [ - { - valueQuantity: { - value: 48, - unit: 'Hour(s)', - system: 'http://unitsofmeasure.org', - code: 'hour' - } - } - ], - text: 'Duration' - }, - { - linkId: 'duration-multi-unit-comparator', - answer: [ - { - valueQuantity: { - value: 48, - comparator: '>=', - unit: 'Hour(s)', - system: 'http://unitsofmeasure.org', - code: 'hour' - } - } - ], - text: 'Duration' - } - ], - questionnaire: 'https://smartforms.csiro.au/docs/components/quantity/unit-option' -}; - -export const qQuantityCalculation: Questionnaire = { - resourceType: 'Questionnaire', - id: 'QuantityCalculation', - name: 'QuantityCalculation', - title: 'Quantity Calculation', - version: '0.1.0', - status: 'draft', - publisher: 'AEHRC CSIRO', - date: '2024-05-01', - url: 'https://smartforms.csiro.au/docs/components/quantity/calculation', - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/variable', - valueExpression: { - name: 'durationInDays', - language: 'text/fhirpath', - expression: "item.where(linkId='duration-in-days').answer.value" - } - } - ], - item: [ - { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit', - valueCoding: { system: 'http://unitsofmeasure.org', code: 'd', display: 'days' } - } - ], - linkId: 'duration-in-days', - type: 'quantity', - repeats: false, - text: 'Duration in Days' - }, - { - extension: [ - { - url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit', - valueCoding: { system: 'http://unitsofmeasure.org', code: 'h', display: 'hours' } - }, - { - url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', - valueExpression: { - description: 'Duration In Hours', - language: 'text/fhirpath', - expression: '%durationInDays.value * 24' - } - } - ], - linkId: 'duration-in-hours', - type: 'quantity', - repeats: false, - text: 'Duration in Hours' - } - ] -}; diff --git a/packages/smart-forms-renderer/src/stories/assets/questionnaires/index.ts b/packages/smart-forms-renderer/src/stories/assets/questionnaires/index.ts index 74057bfe3..1b86ecdd6 100644 --- a/packages/smart-forms-renderer/src/stories/assets/questionnaires/index.ts +++ b/packages/smart-forms-renderer/src/stories/assets/questionnaires/index.ts @@ -17,18 +17,15 @@ export * from './QBoolean'; export * from './QDecimal'; -export * from './QInteger'; export * from './QDate'; export * from './QDateTime'; export * from './QTime'; -export * from './QString'; -export * from './QText'; + export * from './QUrl'; export * from './QChoice'; export * from './QOpenChoice'; export * from './QAttachment'; export * from './QReference'; -export * from './QQuantity'; export * from './QAdvancedAdditionalDisplayContent'; export * from './QAdvancedControlAppearance'; export * from './QAdvancedOther'; diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx index d6b737fe7..b89b54510 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx @@ -9,7 +9,13 @@ import { questionnaireFactory, variableExtFactory } from '../testUtils'; -import { chooseSelectOption, inputDecimal, inputText } from '@aehrc/testing-toolkit'; +import { + chooseSelectOption, + inputInteger, + inputQuantity, + inputDecimal, + inputText +} from '@aehrc/testing-toolkit'; import { expect, waitFor } from 'storybook/test'; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export @@ -190,6 +196,92 @@ export const ChoiceAnswerValueSetCalculation: Story = { }); } }; + +const integerLinkId = 'length-controller'; +const integerLinkIdCalc = 'length-squared'; +const qIntegerCalculation = questionnaireFactory( + [ + { + linkId: integerLinkId, + type: 'integer' + }, + { + extension: [calculatedExpressionExtFactory('%length.power(2)')], + linkId: integerLinkIdCalc, + type: 'integer', + readOnly: true + } + ], + { + extension: [ + variableExtFactory('length', `item.where(linkId = '${integerLinkId}').answer.value`) + ] + } +); +const integerTargetNumber = 2; +const integerTargetNumberCalc = 4; + +export const IntegerCalculation: Story = { + args: { + questionnaire: qIntegerCalculation + }, + play: async ({ canvasElement }) => { + await inputInteger(canvasElement, integerLinkId, integerTargetNumber); + + await waitFor(async () => { + const result = await getAnswers(integerLinkIdCalc); + expect(result).toHaveLength(1); + expect(result[0].valueInteger).toBe(integerTargetNumberCalc); + }); + } +}; + +const quantityDaysLinkId = 'duration-in-days'; +const quantityHoursLinkId = 'duration-in-hours'; +const quantityTargetDays = 1; +const quantityTargetHours = 24; +const qQuantityCalculation = questionnaireFactory( + [ + { + linkId: quantityDaysLinkId, + type: 'quantity' + }, + { + extension: [calculatedExpressionExtFactory('%durationInDays.value * 24')], + linkId: quantityHoursLinkId, + type: 'quantity' + } + ], + { + extension: [ + variableExtFactory( + 'durationInDays', + `item.where(linkId='${quantityDaysLinkId}').answer.value` + ) + ] + } +); +const quantityTarget = { + unit: undefined, + value: quantityTargetHours, + code: undefined, + system: undefined +}; + +export const QuantityCalculation: Story = { + args: { + questionnaire: qQuantityCalculation + }, + play: async ({ canvasElement }) => { + await inputQuantity(canvasElement, quantityDaysLinkId, quantityTargetDays); + + await waitFor(async () => { + const result = await getAnswers(quantityHoursLinkId); + expect(result).toHaveLength(1); + expect(result[0].valueQuantity).toEqual(expect.objectContaining(quantityTarget)); + }); + } +}; const heightLinkId = 'patient-height'; const weightLinkId = 'patient-weight'; const bmiLinkIdCalc = 'bmi-result'; diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Integer.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Integer.stories.tsx index f7b61714f..7e03f8083 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Integer.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Integer.stories.tsx @@ -17,7 +17,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { qIntegerCalculation } from '../assets/questionnaires'; import { findByLinkId, getInputText, inputInteger } from '@aehrc/testing-toolkit'; import { expect, fireEvent } from 'storybook/test'; import { getAnswers, qrFactory, questionnaireFactory } from '../testUtils'; @@ -93,9 +92,3 @@ export const IntegerBasicResponse: Story = { expect(input).toBe(targetAge.toString()); } }; - -export const IntegerCalculation: Story = { - args: { - questionnaire: qIntegerCalculation - } -}; diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Quantity.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Quantity.stories.tsx index 4fa34e4f5..6e5402858 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Quantity.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Quantity.stories.tsx @@ -16,14 +16,18 @@ */ import type { Meta, StoryObj } from '@storybook/react-vite'; +import { expect, waitFor } from 'storybook/test'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; import { - qQuantityBasic, - qQuantityCalculation, - qQuantityUnitOption, - qrQuantityBasicResponse, - qrQuantityUnitOptionResponse -} from '../assets/questionnaires'; + getAnswers, + qrFactory, + questionnaireFactory, + ucumSystem, + unitExtFactory, + unitOptionExtFactory +} from '../testUtils'; +import { getQuantityTextValues, inputQuantity } from '@aehrc/testing-toolkit'; +import type { Quantity } from 'fhir/r4'; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export const meta = { @@ -37,35 +41,366 @@ export default meta; type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +const basicLinkId = 'body-weight'; +const basicUnit = 'kg'; +const basicCode = 'kg'; +const qQuantityBasic = questionnaireFactory([ + { + linkId: basicLinkId, + extension: [unitExtFactory(basicUnit, basicCode)], + type: 'quantity' + } +]); +const basicTargetNumber = 10; export const QuantityBasic: Story = { args: { questionnaire: qQuantityBasic + }, + play: async ({ canvasElement }) => { + await inputQuantity(canvasElement, basicLinkId, basicTargetNumber); + + await waitFor(async () => { + const result = await getAnswers(basicLinkId); + expect(result).toHaveLength(1); + expect(result[0].valueQuantity).toEqual( + expect.objectContaining({ + value: basicTargetNumber, + comparator: undefined, + unit: basicUnit, + code: basicCode, + system: ucumSystem + }) + ); + }); + } +}; + +const basicComparatorLinkId = 'body-weight-comparator'; +const basicComparatorUnit = 'kg'; +const basicComparatorCode = 'kg'; +const qQuantityBasicComparator = questionnaireFactory([ + { + linkId: basicComparatorLinkId, + extension: [unitExtFactory(basicComparatorUnit, basicComparatorCode)], + type: 'quantity' + } +]); +const basicComparatorTargetNumber = 20; +const basicTargetComparator = '<' as const; + +export const QuantityBasicComparator: Story = { + args: { + questionnaire: qQuantityBasicComparator + }, + play: async ({ canvasElement }) => { + await inputQuantity( + canvasElement, + basicComparatorLinkId, + basicComparatorTargetNumber, + undefined, + basicTargetComparator + ); + + await waitFor(async () => { + const result = await getAnswers(basicComparatorLinkId); + expect(result).toHaveLength(1); + expect(result[0].valueQuantity).toEqual( + expect.objectContaining({ + value: basicComparatorTargetNumber, + comparator: basicTargetComparator, + system: ucumSystem, + unit: basicComparatorUnit, + code: basicComparatorCode + }) + ); + }); } }; +const basicResTargetLinkId = 'body-weight'; +const basicResTargetWeight = 80; + +const qrQuantityBasicResponse = qrFactory([ + { + linkId: basicResTargetLinkId, + answer: [ + { + valueQuantity: { + value: basicResTargetWeight, + system: ucumSystem + } + } + ] + } +]); + export const QuantityBasicResponse: Story = { args: { questionnaire: qQuantityBasic, questionnaireResponse: qrQuantityBasicResponse + }, + play: async ({ canvasElement }) => { + const result = await getQuantityTextValues(canvasElement, basicResTargetLinkId, false); + + expect(result).toEqual( + expect.objectContaining({ + value: basicResTargetWeight.toString(), + comparator: '', + unit: undefined + }) + ); + } +}; + +const basicResComparatorLinkId = 'body-weight-comparator'; +const basicResComparatorTargetWeight = 100; +const basicResTargetComparator = '<' as const; + +const qrQuantityBasicComparatorResponse = qrFactory([ + { + linkId: basicResComparatorLinkId, + answer: [ + { + valueQuantity: { + value: basicResComparatorTargetWeight, + comparator: basicResTargetComparator, + system: ucumSystem + } + } + ] + } +]); + +export const QuantityBasicComparatorResponse: Story = { + args: { + questionnaire: qQuantityBasicComparator, + questionnaireResponse: qrQuantityBasicComparatorResponse + }, + play: async ({ canvasElement }) => { + const resultComparator = await getQuantityTextValues( + canvasElement, + basicResComparatorLinkId, + false + ); + + expect(resultComparator).toEqual( + expect.objectContaining({ + value: basicResComparatorTargetWeight.toString(), + comparator: basicResTargetComparator, + unit: undefined + }) + ); + } +}; + +const singleUnitLinkId = 'duration-single-unit'; +const singleTargetNumber = 10; +const singleTargetUnit = 'Day(s)'; +const qQuantitySingle = questionnaireFactory([ + { + linkId: singleUnitLinkId, + type: 'quantity', + extension: [unitExtFactory('d', singleTargetUnit)] + } +]); + +export const QuantitySingleUnit: Story = { + args: { + questionnaire: qQuantitySingle + }, + play: async ({ canvasElement }) => { + await inputQuantity(canvasElement, singleUnitLinkId, singleTargetNumber); + + await waitFor(async () => { + const result = await getAnswers(singleUnitLinkId); + expect(result).toHaveLength(1); + expect(result[0].valueQuantity).toEqual( + expect.objectContaining({ + value: singleTargetNumber, + unit: singleTargetUnit, + code: 'd', + comparator: undefined, + system: ucumSystem + }) + ); + }); } }; -export const QuantityUnitOption: Story = { +const multiLinkId = 'duration-multi-unit'; +const multiTargetNumber = 20; +const multiTargetUnit = 'Week(s)'; + +const qQuantityMulti = questionnaireFactory([ + { + linkId: multiLinkId, + type: 'quantity', + extension: [unitOptionExtFactory('d', 'Day(s)'), unitOptionExtFactory('wk', multiTargetUnit)] + } +]); + +export const QuantityMultiUnit: Story = { args: { - questionnaire: qQuantityUnitOption + questionnaire: qQuantityMulti + }, + play: async ({ canvasElement }) => { + await inputQuantity(canvasElement, multiLinkId, multiTargetNumber, multiTargetUnit); + + await waitFor(async () => { + const result = await getAnswers(multiLinkId); + expect(result).toHaveLength(1); + expect(result[0].valueQuantity).toEqual( + expect.objectContaining({ + value: multiTargetNumber, + unit: multiTargetUnit, + comparator: undefined, + system: ucumSystem + }) + ); + }); } }; -export const QuantityUnitOptionResponse: Story = { +const multiComparatorLinkId = 'duration-multi-unit-comparator'; +const multiComparatorTargetNumber = 30; +const multiComparatorTargetUnit = 'Day(s)'; +const multiComparatorTargetComparator = '>'; + +const qQuantityMultiComparator = questionnaireFactory([ + { + linkId: multiComparatorLinkId, + type: 'quantity', + extension: [ + unitOptionExtFactory('d', multiComparatorTargetUnit), + unitOptionExtFactory('wk', 'Week(s)') + ] + } +]); + +export const QuantityMultiUnitComparator: Story = { args: { - questionnaire: qQuantityUnitOption, - questionnaireResponse: qrQuantityUnitOptionResponse + questionnaire: qQuantityMultiComparator + }, + play: async ({ canvasElement }) => { + await inputQuantity( + canvasElement, + multiComparatorLinkId, + multiComparatorTargetNumber, + multiComparatorTargetUnit, + multiComparatorTargetComparator + ); + + await waitFor(async () => { + const result = await getAnswers(multiComparatorLinkId); + expect(result).toHaveLength(1); + expect(result[0].valueQuantity).toEqual( + expect.objectContaining({ + value: multiComparatorTargetNumber, + unit: multiComparatorTargetUnit, + comparator: multiComparatorTargetComparator, + system: ucumSystem + }) + ); + }); } }; +const unitsingleResLinkId = 'duration-single-unit'; +const unitsingleQuantity: Quantity = { + value: 2, + unit: 'Day(s)', + system: ucumSystem, + code: 'd' +}; +const qrQuantityUnitOptionSingleResponse = qrFactory([ + { + linkId: unitsingleResLinkId, + answer: [{ valueQuantity: unitsingleQuantity }] + } +]); + +export const QuantitySingleUnitOptionResponse: Story = { + args: { + questionnaire: qQuantitySingle, + questionnaireResponse: qrQuantityUnitOptionSingleResponse + }, + play: async ({ canvasElement }) => { + const resultSingle = await getQuantityTextValues(canvasElement, unitsingleResLinkId, false); + + expect(resultSingle).toEqual( + expect.objectContaining({ + value: unitsingleQuantity?.value?.toString(), + unit: undefined, + comparator: '' + }) + ); + } +}; + +const unitmultiResLinkId = 'duration-multi-unit'; +const unitmultiResQuantity: Quantity = { + value: 48, + unit: 'Hour(s)', + system: ucumSystem, + code: 'hour' +}; +const qrQuantityUnitOptionMultiResponse = qrFactory([ + { + linkId: unitmultiResLinkId, + answer: [{ valueQuantity: unitmultiResQuantity }] + } +]); + +export const QuantityMultiUnitOptionResponse: Story = { + args: { + questionnaire: qQuantityMulti, + questionnaireResponse: qrQuantityUnitOptionMultiResponse + }, + play: async ({ canvasElement }) => { + const resultMulti = await getQuantityTextValues(canvasElement, unitmultiResLinkId, true); + expect(resultMulti).toEqual( + expect.objectContaining({ + value: unitmultiResQuantity?.value?.toString(), + unit: unitmultiResQuantity.unit, + comparator: '' + }) + ); + } +}; + +const unitmultiComparatorResLinkId = 'duration-multi-unit-comparator'; +const unitMultiComparatorQuantity: Quantity = { + value: 48, + comparator: '>=', + unit: 'Hour(s)', + system: ucumSystem, + code: 'hour' +}; + +const qrQuantityUnitOptionMultiComparatorResponse = qrFactory([ + { + linkId: unitmultiComparatorResLinkId, + answer: [{ valueQuantity: unitMultiComparatorQuantity }] + } +]); -export const QuantityCalculation: Story = { +export const QuantityMultiUnitOptionComparatorResponse: Story = { args: { - questionnaire: qQuantityCalculation + questionnaire: qQuantityMultiComparator, + questionnaireResponse: qrQuantityUnitOptionMultiComparatorResponse + }, + play: async ({ canvasElement }) => { + const resultMultiComparator = await getQuantityTextValues( + canvasElement, + unitmultiComparatorResLinkId, + true + ); + expect(resultMultiComparator).toEqual( + expect.objectContaining({ + value: unitMultiComparatorQuantity.value?.toString(), + unit: unitMultiComparatorQuantity.unit, + comparator: unitMultiComparatorQuantity.comparator + }) + ); } }; diff --git a/packages/smart-forms-renderer/src/stories/testUtils.ts b/packages/smart-forms-renderer/src/stories/testUtils.ts index b959fd1cd..30a061369 100644 --- a/packages/smart-forms-renderer/src/stories/testUtils.ts +++ b/packages/smart-forms-renderer/src/stories/testUtils.ts @@ -86,6 +86,27 @@ export function variableExtFactory(name: string, text: string): Extension { }; } +export function unitOptionExtFactory(code: string, display: string): Extension { + return { + url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption', + valueCoding: { + system: 'http://unitsofmeasure.org', + code: code, + display: display + } + }; +} +export function unitExtFactory(code: string, display: string): Extension { + return { + url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit', + valueCoding: { + system: 'http://unitsofmeasure.org', + code: code, + display: display + } + }; +} + export function сqfExpressionFactory(text: string) { return { url: 'http://hl7.org/fhir/StructureDefinition/cqf-expression', @@ -95,3 +116,5 @@ export function сqfExpressionFactory(text: string) { } }; } + +export const ucumSystem = 'http://unitsofmeasure.org'; diff --git a/packages/testing-toolkit/src/index.ts b/packages/testing-toolkit/src/index.ts index 839db2772..037719ceb 100644 --- a/packages/testing-toolkit/src/index.ts +++ b/packages/testing-toolkit/src/index.ts @@ -11,6 +11,7 @@ export async function inputText( const input = questionElement?.querySelector('input') ?? questionElement?.querySelector('textarea'); + // Error section if (!input) { throw new Error(`Input or textarea was not found inside ${`[data-linkid=${linkId}] block`}`); } @@ -20,11 +21,13 @@ export async function inputText( // Here we await for debounced store update await new Promise((resolve) => setTimeout(resolve, 500)); } + export async function checkCheckBox(canvasElement: HTMLElement, linkId: string) { const questionElement = await findByLinkId(canvasElement, linkId); const input = questionElement?.querySelector('input') ?? questionElement?.querySelector('textarea'); + // Error section if (!input) { throw new Error(`Input or textarea was not found inside ${`[data-linkid=${linkId}] block`}`); } @@ -50,6 +53,7 @@ export async function inputFile( `textarea[data-test="q-item-attachment-file-name"]` ); + // Error section if (!input) { throw new Error(`File input was not found inside [data-linkid=${linkId}] block`); } @@ -69,6 +73,84 @@ export async function inputFile( await new Promise((resolve) => setTimeout(resolve, 500)); } +export async function getQuantityTextValues( + canvasElement: HTMLElement, + linkId: string, + unit: boolean +) { + const element = await findByLinkId(canvasElement, linkId); + const quantityComparator = element.querySelector( + 'div[data-test="q-item-quantity-comparator"] input' + ); + const quantityInput = element.querySelector('div[data-test="q-item-quantity-field"] input'); + const quantityUnit = element.querySelector('div[data-test="q-item-unit-field"] input'); + + // Error section + if (!quantityComparator) { + throw new Error( + `File input was not found inside [data-test="q-item-quantity-comparator"] block` + ); + } + if (!quantityInput) { + throw new Error(`File input was not found inside [data-test="q-item-quantity-field"] block`); + } + if (!quantityUnit && unit) { + throw new Error(`File input was not found inside [data-test="q-item-unit-field"] block`); + } + + return { + comparator: quantityComparator?.getAttribute('value'), + value: quantityInput?.getAttribute('value'), + unit: quantityUnit?.getAttribute('value') + }; +} + +export async function inputQuantity( + canvasElement: HTMLElement, + linkId: string, + quantity: number, + unit?: string, + comparator?: string +) { + const questionElement = await findByLinkId(canvasElement, linkId); + + const comparatorInput = questionElement?.querySelector( + `div[data-test="q-item-quantity-comparator"] input` + ); + const quantityInput = questionElement?.querySelector( + `div[data-test="q-item-quantity-field"] input` + ); + const unitInput = questionElement?.querySelector(`div[data-test="q-item-unit-field"] input`); + + // Error section + if (comparator && !comparatorInput) { + throw new Error(`Input was not found inside [data-test="q-item-quantity-comparator"] block`); + } + if (!quantityInput) { + throw new Error(`Input was not found inside [data-test="q-item-quantity-field"] block`); + } + if (!unitInput && unit) { + throw new Error(`Input was not found inside [data-test="q-item-unit-field"] block`); + } + + if (comparator && comparatorInput) { + fireEvent.focus(comparatorInput); + fireEvent.keyDown(comparatorInput, { key: 'ArrowDown', code: 'ArrowDown' }); + const option = await screen.findByText(comparator); + fireEvent.click(option); + } + if (unit && unitInput) { + fireEvent.focus(unitInput); + fireEvent.keyDown(unitInput, { key: 'ArrowDown', code: 'ArrowDown' }); + const option = await screen.findByText(unit); + fireEvent.click(option); + } + fireEvent.change(quantityInput, { target: { value: quantity } }); + + // Here we await for debounced store update + await new Promise((resolve) => setTimeout(resolve, 500)); +} + export async function inputDate( canvasElement: HTMLElement, linkId: string, @@ -118,14 +200,15 @@ export async function inputDateTime( const inputTime = questionElement?.querySelector('div[data-test="time"] input'); const inputAmPm = questionElement?.querySelector('div[data-test="ampm"] input'); + // Error section if (!inputTime) { - throw new Error(`Input or textarea was not found inside ${`[data-linkid=${linkId}] block`}`); + throw new Error(`Input or textarea was not found inside ${`[data-test="time"] block`}`); } if (!inputDate) { - throw new Error(`Input or textarea was not found inside ${`[data-linkid=${linkId}] block`}`); + throw new Error(`Input or textarea was not found inside ${`[data-test="date"] block`}`); } if (!inputAmPm) { - throw new Error(`Input or textarea was not found inside ${`[data-linkid=${linkId}] block`}`); + throw new Error(`Input or textarea was not found inside ${`[data-test="ampm"] block`}`); } fireEvent.change(inputDate, { target: { value: date } }); @@ -140,8 +223,11 @@ export async function checkRadioOption(canvasElement: HTMLElement, linkId: strin const questionElement = await findByLinkId(canvasElement, linkId); const radio = questionElement?.querySelector(`span[data-test="radio-single-${text}"] input`); + // Error section if (!radio) { - throw new Error(`Input or textarea was not found inside ${`[data-linkid=${linkId}] block`}`); + throw new Error( + `Input or textarea was not found inside ${`[data-test="radio-single-${text}"] block`}` + ); } fireEvent.click(radio); @@ -154,6 +240,7 @@ export async function getInputText(canvasElement: HTMLElement, linkId: string) { const input = questionElement?.querySelector('input') ?? questionElement?.querySelector('textarea'); + // Error section if (!input) { throw new Error(`Input or textarea was not found inside ${`[data-linkid=${linkId}] block`}`); } @@ -169,6 +256,8 @@ export async function chooseSelectOption( const questionElement = await findByLinkId(canvasElement, linkId); const input = questionElement.querySelector('input, textarea'); + + // Error section if (!input) { throw new Error(`There is no input inside ${linkId}`); } @@ -187,28 +276,31 @@ export async function chooseQuantityOption( ) { const questionElement = await findByLinkId(canvasElement, linkId); - const inputComaparator = questionElement.querySelector( - 'div[data-test=""q-item-quantity-comparator""] input' + const inputComparator = questionElement.querySelector( + 'div[data-test="q-item-quantity-comparator"] input' + ); + const inputQuantity = questionElement.querySelector( + 'div[data-test="q-item-quantity-field"] input' ); - const inputWeight = questionElement.querySelector('div[data-test="q-item-quantity-field"] input'); - if (!inputComaparator) { - throw new Error(`There is no input inside ${linkId}`); + // Error section + if (!inputComparator) { + throw new Error(`There is no input inside [data-test="q-item-quantity-comparator"]`); } - if (!inputWeight) { - throw new Error(`There is no input inside ${linkId}`); + if (!inputQuantity) { + throw new Error(`There is no input inside [data-test="q-item-quantity-field"]`); } - fireEvent.focus(inputComaparator); - fireEvent.keyDown(inputComaparator, { key: 'ArrowDown', code: 'ArrowDown' }); + fireEvent.focus(inputComparator); + fireEvent.keyDown(inputComparator, { key: 'ArrowDown', code: 'ArrowDown' }); if (quantityComparator) { const option = await screen.findByText(quantityComparator); fireEvent.click(option); - fireEvent.change(inputComaparator, { target: { value: quantityComparator } }); + fireEvent.change(inputComparator, { target: { value: quantityComparator } }); } - fireEvent.change(inputWeight, { target: { value: quantity } }); + fireEvent.change(inputQuantity, { target: { value: quantity } }); // Here we await for debounced store update await new Promise((resolve) => setTimeout(resolve, 500)); @@ -235,8 +327,11 @@ export async function inputOpenChoiceOtherText( 'div[data-test="q-item-radio-open-label-box"] textarea' ); + // Error section if (!textarea) { - throw new Error(`Input or textarea was not found inside ${`[data-test=${linkId}] block`}`); + throw new Error( + 'Input or textarea was not found inside [data-test="q-item-radio-open-label-box"] block' + ); } fireEvent.change(textarea, { target: { value: text } }); From ea3aea2fd023efa5f77a5693bceb8ec24f8a376f Mon Sep 17 00:00:00 2001 From: Prosvirin Vladimir Date: Wed, 24 Sep 2025 01:19:22 +0300 Subject: [PATCH 5/8] Delete unused imports --- .../src/stories/itemTypes/Choice.stories.tsx | 4 ---- .../src/stories/itemTypes/Decimal.stories.tsx | 2 +- .../src/stories/itemTypes/Integer.stories.tsx | 2 +- .../src/stories/itemTypes/String.stories.tsx | 1 - .../src/stories/itemTypes/Text.stories.tsx | 1 - 5 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Choice.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Choice.stories.tsx index 11153a89a..4aa24a11c 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Choice.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Choice.stories.tsx @@ -17,10 +17,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { - qChoiceAnswerOptionCalculation, - qChoiceAnswerValueSetCalculation -} from '../assets/questionnaires'; import { chooseSelectOption, findByLinkId, diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Decimal.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Decimal.stories.tsx index 0981ffe29..d610d0269 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Decimal.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Decimal.stories.tsx @@ -17,7 +17,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { qCalculatedExpressionBMICalculator } from '../assets/questionnaires'; + import { findByLinkId, getAnswers, diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Integer.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Integer.stories.tsx index 92c537342..b5f64d742 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Integer.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Integer.stories.tsx @@ -17,7 +17,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { qIntegerCalculation } from '../assets/questionnaires'; + import { findByLinkId, getAnswers, diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/String.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/String.stories.tsx index 23c207b2a..cd3258c53 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/String.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/String.stories.tsx @@ -17,7 +17,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { qStringCalculation } from '../assets/questionnaires'; import { findByLinkId, getAnswers, diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Text.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Text.stories.tsx index 64a398dae..a108ccc74 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Text.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Text.stories.tsx @@ -17,7 +17,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { qTextCalculation } from '../assets/questionnaires'; import { findByLinkId, getAnswers, From a1047b73e2d252802406f5124fc949b5714b1054 Mon Sep 17 00:00:00 2001 From: Prosvirin Vladimir Date: Tue, 30 Sep 2025 01:22:19 +0300 Subject: [PATCH 6/8] Update item types --- .../itemTypes/AllCalculations.stories.tsx | 7 +- .../itemTypes/AllCqfExpressions.stories.tsx | 9 ++- .../stories/itemTypes/Quantity.stories.tsx | 6 +- .../src/stories/testUtils.ts | 81 ++++++++++++++++++- 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx index b89b54510..150ba6b8d 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/AllCalculations.stories.tsx @@ -7,15 +7,14 @@ import { getGroupAnswers, itemControlExtFactory, questionnaireFactory, - variableExtFactory -} from '../testUtils'; -import { + variableExtFactory, chooseSelectOption, inputInteger, inputQuantity, inputDecimal, inputText -} from '@aehrc/testing-toolkit'; +} from '../testUtils'; + import { expect, waitFor } from 'storybook/test'; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/AllCqfExpressions.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/AllCqfExpressions.stories.tsx index 666a5dbe6..d92197c9f 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/AllCqfExpressions.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/AllCqfExpressions.stories.tsx @@ -18,8 +18,13 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { expect, within } from 'storybook/test'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; -import { chooseSelectOption } from '@aehrc/testing-toolkit'; -import { questionnaireFactory, variableExtFactory, сqfExpressionFactory } from '../testUtils'; + +import { + chooseSelectOption, + questionnaireFactory, + variableExtFactory, + сqfExpressionFactory +} from '../testUtils'; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export const meta = { diff --git a/packages/smart-forms-renderer/src/stories/itemTypes/Quantity.stories.tsx b/packages/smart-forms-renderer/src/stories/itemTypes/Quantity.stories.tsx index 6e5402858..c52fae8f8 100644 --- a/packages/smart-forms-renderer/src/stories/itemTypes/Quantity.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/itemTypes/Quantity.stories.tsx @@ -24,9 +24,11 @@ import { questionnaireFactory, ucumSystem, unitExtFactory, - unitOptionExtFactory + unitOptionExtFactory, + inputQuantity, + getQuantityTextValues } from '../testUtils'; -import { getQuantityTextValues, inputQuantity } from '@aehrc/testing-toolkit'; + import type { Quantity } from 'fhir/r4'; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export diff --git a/packages/smart-forms-renderer/src/stories/testUtils.ts b/packages/smart-forms-renderer/src/stories/testUtils.ts index 5509d8103..0c1d8d1c7 100644 --- a/packages/smart-forms-renderer/src/stories/testUtils.ts +++ b/packages/smart-forms-renderer/src/stories/testUtils.ts @@ -7,8 +7,85 @@ import type { QuestionnaireResponse, QuestionnaireResponseItem } from 'fhir/r4'; -import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { userEvent } from '@testing-library/user-event'; +import { fireEvent, screen, userEvent, waitFor } from 'storybook/internal/test'; + +export async function getQuantityTextValues( + canvasElement: HTMLElement, + linkId: string, + unit: boolean +) { + const element = await findByLinkId(canvasElement, linkId); + const quantityComparator = element.querySelector( + 'div[data-test="q-item-quantity-comparator"] input' + ); + const quantityInput = element.querySelector('div[data-test="q-item-quantity-field"] input'); + const quantityUnit = element.querySelector('div[data-test="q-item-unit-field"] input'); + + // Error section + if (!quantityComparator) { + throw new Error( + `File input was not found inside [data-test="q-item-quantity-comparator"] block` + ); + } + if (!quantityInput) { + throw new Error(`File input was not found inside [data-test="q-item-quantity-field"] block`); + } + if (!quantityUnit && unit) { + throw new Error(`File input was not found inside [data-test="q-item-unit-field"] block`); + } + + return { + comparator: quantityComparator?.getAttribute('value'), + value: quantityInput?.getAttribute('value'), + unit: quantityUnit?.getAttribute('value') + }; +} + +export async function inputQuantity( + canvasElement: HTMLElement, + linkId: string, + quantity: number, + unit?: string, + comparator?: string +) { + const questionElement = await findByLinkId(canvasElement, linkId); + + const comparatorInput = questionElement?.querySelector( + `div[data-test="q-item-quantity-comparator"] input` + ); + const quantityInput = questionElement?.querySelector( + `div[data-test="q-item-quantity-field"] input` + ); + const unitInput = questionElement?.querySelector(`div[data-test="q-item-unit-field"] input`); + + // Error section + if (comparator && !comparatorInput) { + throw new Error(`Input was not found inside [data-test="q-item-quantity-comparator"] block`); + } + if (!quantityInput) { + throw new Error(`Input was not found inside [data-test="q-item-quantity-field"] block`); + } + if (!unitInput && unit) { + throw new Error(`Input was not found inside [data-test="q-item-unit-field"] block`); + } + + if (comparator && comparatorInput) { + fireEvent.focus(comparatorInput); + fireEvent.keyDown(comparatorInput, { key: 'ArrowDown', code: 'ArrowDown' }); + const option = await screen.findByText(comparator); + fireEvent.click(option); + } + if (unit && unitInput) { + fireEvent.focus(unitInput); + fireEvent.keyDown(unitInput, { key: 'ArrowDown', code: 'ArrowDown' }); + const option = await screen.findByText(unit); + fireEvent.click(option); + } + fireEvent.change(quantityInput, { target: { value: quantity } }); + + // Here we await for debounced store update + await new Promise((resolve) => setTimeout(resolve, 500)); +} export async function getAnswers(linkId: string) { const qr = questionnaireResponseStore.getState().updatableResponse; From 61525e59d20cef711b6b594aaf66184a70ae1c1c Mon Sep 17 00:00:00 2001 From: Prosvirin Vladimir Date: Tue, 30 Sep 2025 01:37:51 +0300 Subject: [PATCH 7/8] Delete testing-toolkit package --- apps/smart-forms-app/package.json | 1 - package-lock.json | 31 +- packages/testing-toolkit/.gitignore | 5 - packages/testing-toolkit/package.json | 29 -- packages/testing-toolkit/src/index.ts | 341 --------------------- packages/testing-toolkit/src/vite-env.d.ts | 27 -- packages/testing-toolkit/tsconfig.json | 22 -- packages/testing-toolkit/vite.config.ts | 28 -- 8 files changed, 10 insertions(+), 474 deletions(-) delete mode 100644 packages/testing-toolkit/.gitignore delete mode 100644 packages/testing-toolkit/package.json delete mode 100644 packages/testing-toolkit/src/index.ts delete mode 100644 packages/testing-toolkit/src/vite-env.d.ts delete mode 100644 packages/testing-toolkit/tsconfig.json delete mode 100644 packages/testing-toolkit/vite.config.ts diff --git a/apps/smart-forms-app/package.json b/apps/smart-forms-app/package.json index cb51064fd..574ce2f7a 100644 --- a/apps/smart-forms-app/package.json +++ b/apps/smart-forms-app/package.json @@ -28,7 +28,6 @@ "@aehrc/sdc-populate": "^4.6.2", "@aehrc/sdc-template-extract": "^1.0.9", "@aehrc/smart-forms-renderer": "^1.0.0-alpha.107", - "@aehrc/testing-toolkit": "^1.0.0", "@dnd-kit/core": "^6.3.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", diff --git a/package-lock.json b/package-lock.json index fae05c761..dff1c1d1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,6 @@ "@aehrc/sdc-populate": "^4.6.2", "@aehrc/sdc-template-extract": "^1.0.9", "@aehrc/smart-forms-renderer": "^1.0.0-alpha.107", - "@aehrc/testing-toolkit": "^1.0.0", "@dnd-kit/core": "^6.3.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", @@ -1071,10 +1070,6 @@ "resolved": "packages/smart-forms-renderer", "link": true }, - "node_modules/@aehrc/testing-toolkit": { - "resolved": "packages/testing-toolkit", - "link": true - }, "node_modules/@algolia/abtesting": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.3.0.tgz", @@ -12118,6 +12113,7 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -12163,6 +12159,7 @@ "version": "16.3.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" @@ -12244,6 +12241,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, "peer": true }, "node_modules/@types/babel__core": { @@ -12797,7 +12795,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -14143,6 +14141,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, "dependencies": { "dequal": "^2.0.3" } @@ -18018,6 +18017,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, "peer": true }, "node_modules/dom-converter": { @@ -25598,6 +25598,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, "peer": true, "bin": { "lz-string": "bin/bin.js" @@ -32135,6 +32136,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "peer": true, "dependencies": { "ansi-regex": "^5.0.1", @@ -32149,6 +32151,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "peer": true, "engines": { "node": ">=10" @@ -32161,6 +32164,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, "peer": true }, "node_modules/pretty-time": { @@ -41210,21 +41214,6 @@ "url": "https://opencollective.com/unified" } }, - "packages/testing-toolkit": { - "name": "@aehrc/testing-toolkit", - "version": "1.0.0", - "license": "Apache-2.0", - "dependencies": { - "@testing-library/react": "^16.3.0" - }, - "devDependencies": { - "zustand": "^5.0.8" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, "services/assemble-express": { "version": "2.0.0", "license": "Apache-2.0", diff --git a/packages/testing-toolkit/.gitignore b/packages/testing-toolkit/.gitignore deleted file mode 100644 index 945ef5a39..000000000 --- a/packages/testing-toolkit/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -lib -dist -temp - diff --git a/packages/testing-toolkit/package.json b/packages/testing-toolkit/package.json deleted file mode 100644 index eb7132831..000000000 --- a/packages/testing-toolkit/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@aehrc/testing-toolkit", - "version": "1.0.0", - "description": "Testing toolkit for Smart Forms", - "main": "lib/index.js", - "scripts": { - "compile": "tsc", - "watch": "tsc -w", - "build": "npm run compile", - "test": "jest", - "test:watch": "jest --watch", - "storybook": "storybook dev -p 6006", - "storybook-watch": "concurrently -n \"STORYBOOK,TSC\" -c \"cyan.bold,green.bold\" \"storybook dev -p 6006\" \"tsc -w\"", - "build-storybook": "storybook build", - "chromatic": "chromatic --exit-zero-on-changes" - }, - "license": "Apache-2.0", - "homepage": "https://github.com/aehrc/smart-forms#readme", - "dependencies": { - "@testing-library/react": "^16.3.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "devDependencies": { - "zustand": "^5.0.8" - } -} diff --git a/packages/testing-toolkit/src/index.ts b/packages/testing-toolkit/src/index.ts deleted file mode 100644 index 037719ceb..000000000 --- a/packages/testing-toolkit/src/index.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { userEvent } from '@testing-library/user-event'; - -export async function inputText( - canvasElement: HTMLElement, - linkId: string, - text: string | boolean | number -) { - const questionElement = await findByLinkId(canvasElement, linkId); - - const input = - questionElement?.querySelector('input') ?? questionElement?.querySelector('textarea'); - - // Error section - if (!input) { - throw new Error(`Input or textarea was not found inside ${`[data-linkid=${linkId}] block`}`); - } - - fireEvent.change(input, { target: { value: text } }); - - // Here we await for debounced store update - await new Promise((resolve) => setTimeout(resolve, 500)); -} - -export async function checkCheckBox(canvasElement: HTMLElement, linkId: string) { - const questionElement = await findByLinkId(canvasElement, linkId); - const input = - questionElement?.querySelector('input') ?? questionElement?.querySelector('textarea'); - - // Error section - if (!input) { - throw new Error(`Input or textarea was not found inside ${`[data-linkid=${linkId}] block`}`); - } - - fireEvent.click(input); - - // Here we await for debounced store update - await new Promise((resolve) => setTimeout(resolve, 500)); -} - -export async function inputFile( - canvasElement: HTMLElement, - linkId: string, - files: File | File[], - url: string, - filename: string -) { - const questionElement = await findByLinkId(canvasElement, linkId); - const input = questionElement?.querySelector('input'); - - const textareaUrl = questionElement?.querySelector(`textarea[data-test="q-item-attachment-url"]`); - const textareaName = questionElement?.querySelector( - `textarea[data-test="q-item-attachment-file-name"]` - ); - - // Error section - if (!input) { - throw new Error(`File input was not found inside [data-linkid=${linkId}] block`); - } - if (!textareaUrl) { - throw new Error(`File input was not found inside [data-linkid="URL"] block`); - } - if (!textareaName) { - throw new Error(`File input was not found inside [data-linkid="File name (optional)"] block`); - } - - const fileList = Array.isArray(files) ? files : [files]; - await userEvent.upload(input, fileList); - - fireEvent.change(textareaUrl, { target: { value: url } }); - fireEvent.change(textareaName, { target: { value: filename } }); - // Here we await for debounced store update - await new Promise((resolve) => setTimeout(resolve, 500)); -} - -export async function getQuantityTextValues( - canvasElement: HTMLElement, - linkId: string, - unit: boolean -) { - const element = await findByLinkId(canvasElement, linkId); - const quantityComparator = element.querySelector( - 'div[data-test="q-item-quantity-comparator"] input' - ); - const quantityInput = element.querySelector('div[data-test="q-item-quantity-field"] input'); - const quantityUnit = element.querySelector('div[data-test="q-item-unit-field"] input'); - - // Error section - if (!quantityComparator) { - throw new Error( - `File input was not found inside [data-test="q-item-quantity-comparator"] block` - ); - } - if (!quantityInput) { - throw new Error(`File input was not found inside [data-test="q-item-quantity-field"] block`); - } - if (!quantityUnit && unit) { - throw new Error(`File input was not found inside [data-test="q-item-unit-field"] block`); - } - - return { - comparator: quantityComparator?.getAttribute('value'), - value: quantityInput?.getAttribute('value'), - unit: quantityUnit?.getAttribute('value') - }; -} - -export async function inputQuantity( - canvasElement: HTMLElement, - linkId: string, - quantity: number, - unit?: string, - comparator?: string -) { - const questionElement = await findByLinkId(canvasElement, linkId); - - const comparatorInput = questionElement?.querySelector( - `div[data-test="q-item-quantity-comparator"] input` - ); - const quantityInput = questionElement?.querySelector( - `div[data-test="q-item-quantity-field"] input` - ); - const unitInput = questionElement?.querySelector(`div[data-test="q-item-unit-field"] input`); - - // Error section - if (comparator && !comparatorInput) { - throw new Error(`Input was not found inside [data-test="q-item-quantity-comparator"] block`); - } - if (!quantityInput) { - throw new Error(`Input was not found inside [data-test="q-item-quantity-field"] block`); - } - if (!unitInput && unit) { - throw new Error(`Input was not found inside [data-test="q-item-unit-field"] block`); - } - - if (comparator && comparatorInput) { - fireEvent.focus(comparatorInput); - fireEvent.keyDown(comparatorInput, { key: 'ArrowDown', code: 'ArrowDown' }); - const option = await screen.findByText(comparator); - fireEvent.click(option); - } - if (unit && unitInput) { - fireEvent.focus(unitInput); - fireEvent.keyDown(unitInput, { key: 'ArrowDown', code: 'ArrowDown' }); - const option = await screen.findByText(unit); - fireEvent.click(option); - } - fireEvent.change(quantityInput, { target: { value: quantity } }); - - // Here we await for debounced store update - await new Promise((resolve) => setTimeout(resolve, 500)); -} - -export async function inputDate( - canvasElement: HTMLElement, - linkId: string, - text: string | boolean -) { - return await inputText(canvasElement, linkId, text); -} - -export async function inputTime( - canvasElement: HTMLElement, - linkId: string, - text: string | boolean -) { - return await inputText(canvasElement, linkId, text); -} - -export async function inputReference( - canvasElement: HTMLElement, - linkId: string, - text: string | boolean -) { - return await inputText(canvasElement, linkId, text); -} - -export async function inputDecimal(canvasElement: HTMLElement, linkId: string, text: number) { - return await inputText(canvasElement, linkId, text); -} - -export async function inputUrl(canvasElement: HTMLElement, linkId: string, text: string) { - return await inputText(canvasElement, linkId, text); -} - -export async function inputInteger(canvasElement: HTMLElement, linkId: string, text: number) { - return await inputText(canvasElement, linkId, text); -} - -export async function inputDateTime( - canvasElement: HTMLElement, - linkId: string, - date: string, - time: string, - amPm: string -) { - const questionElement = await findByLinkId(canvasElement, linkId); - console.log(questionElement, 777); - const inputDate = questionElement?.querySelector('div[data-test="date"] input'); - const inputTime = questionElement?.querySelector('div[data-test="time"] input'); - const inputAmPm = questionElement?.querySelector('div[data-test="ampm"] input'); - - // Error section - if (!inputTime) { - throw new Error(`Input or textarea was not found inside ${`[data-test="time"] block`}`); - } - if (!inputDate) { - throw new Error(`Input or textarea was not found inside ${`[data-test="date"] block`}`); - } - if (!inputAmPm) { - throw new Error(`Input or textarea was not found inside ${`[data-test="ampm"] block`}`); - } - - fireEvent.change(inputDate, { target: { value: date } }); - fireEvent.change(inputTime, { target: { value: time } }); - fireEvent.change(inputAmPm, { target: { value: amPm } }); - - // Here we await for debounced store update - await new Promise((resolve) => setTimeout(resolve, 500)); -} - -export async function checkRadioOption(canvasElement: HTMLElement, linkId: string, text: string) { - const questionElement = await findByLinkId(canvasElement, linkId); - const radio = questionElement?.querySelector(`span[data-test="radio-single-${text}"] input`); - - // Error section - if (!radio) { - throw new Error( - `Input or textarea was not found inside ${`[data-test="radio-single-${text}"] block`}` - ); - } - - fireEvent.click(radio); - // Here we await for debounced store update - await new Promise((resolve) => setTimeout(resolve, 500)); -} - -export async function getInputText(canvasElement: HTMLElement, linkId: string) { - const questionElement = await findByLinkId(canvasElement, linkId); - const input = - questionElement?.querySelector('input') ?? questionElement?.querySelector('textarea'); - - // Error section - if (!input) { - throw new Error(`Input or textarea was not found inside ${`[data-linkid=${linkId}] block`}`); - } - - return input.value; -} - -export async function chooseSelectOption( - canvasElement: HTMLElement, - linkId: string, - optionLabel: string -) { - const questionElement = await findByLinkId(canvasElement, linkId); - - const input = questionElement.querySelector('input, textarea'); - - // Error section - if (!input) { - throw new Error(`There is no input inside ${linkId}`); - } - - fireEvent.focus(input); - fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); - - const option = await screen.findByText(optionLabel); - fireEvent.click(option); -} -export async function chooseQuantityOption( - canvasElement: HTMLElement, - linkId: string, - quantity: number | string, - quantityComparator?: string -) { - const questionElement = await findByLinkId(canvasElement, linkId); - - const inputComparator = questionElement.querySelector( - 'div[data-test="q-item-quantity-comparator"] input' - ); - const inputQuantity = questionElement.querySelector( - 'div[data-test="q-item-quantity-field"] input' - ); - - // Error section - if (!inputComparator) { - throw new Error(`There is no input inside [data-test="q-item-quantity-comparator"]`); - } - if (!inputQuantity) { - throw new Error(`There is no input inside [data-test="q-item-quantity-field"]`); - } - - fireEvent.focus(inputComparator); - fireEvent.keyDown(inputComparator, { key: 'ArrowDown', code: 'ArrowDown' }); - - if (quantityComparator) { - const option = await screen.findByText(quantityComparator); - fireEvent.click(option); - fireEvent.change(inputComparator, { target: { value: quantityComparator } }); - } - - fireEvent.change(inputQuantity, { target: { value: quantity } }); - - // Here we await for debounced store update - await new Promise((resolve) => setTimeout(resolve, 500)); -} - -export async function findByLinkId(canvasElement: HTMLElement, linkId: string) { - const selector = `[data-linkid="${linkId}"]`; - return await waitFor(() => { - const el = canvasElement.querySelector(selector); - if (!el) { - throw new Error(`Element ${selector} not found`); - } - return el; - }); -} -export async function inputOpenChoiceOtherText( - canvasElement: HTMLElement, - linkId: string, - text: string -) { - const questionElement = await findByLinkId(canvasElement, linkId); - - const textarea = questionElement?.querySelector( - 'div[data-test="q-item-radio-open-label-box"] textarea' - ); - - // Error section - if (!textarea) { - throw new Error( - 'Input or textarea was not found inside [data-test="q-item-radio-open-label-box"] block' - ); - } - - fireEvent.change(textarea, { target: { value: text } }); - - // Here we await for debounced store update - await new Promise((resolve) => setTimeout(resolve, 500)); -} diff --git a/packages/testing-toolkit/src/vite-env.d.ts b/packages/testing-toolkit/src/vite-env.d.ts deleted file mode 100644 index 9921b7f24..000000000 --- a/packages/testing-toolkit/src/vite-env.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2023 Commonwealth Scientific and Industrial Research - * Organisation (CSIRO) ABN 41 687 119 230. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/// -// Vite is only used for Storybook - -interface ImportMetaEnv { - readonly VITE_RENDERER_VERSION: string; -} - -interface ImportMeta { - readonly env: ImportMetaEnv; -} diff --git a/packages/testing-toolkit/tsconfig.json b/packages/testing-toolkit/tsconfig.json deleted file mode 100644 index 7508ed78e..000000000 --- a/packages/testing-toolkit/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ES6", - "module": "ES6", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "moduleResolution": "node", - "forceConsistentCasingInFileNames": true, - "useUnknownInCatchVariables": false, - "strict": true, - "skipLibCheck": true, - "jsx": "react-jsx", - "sourceMap": true, - "allowJs": true, - "outDir": "lib", - "declaration": true, - "checkJs": true, - "resolveJsonModule": true - }, - "include": ["src"], - "exclude": ["lib", "dist"] -} diff --git a/packages/testing-toolkit/vite.config.ts b/packages/testing-toolkit/vite.config.ts deleted file mode 100644 index bf414cad3..000000000 --- a/packages/testing-toolkit/vite.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2025 Commonwealth Scientific and Industrial Research - * Organisation (CSIRO) ABN 41 687 119 230. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { defineConfig } from 'vite'; -// @ts-ignore -import { version } from './package.json'; - -// This Vite config is for storybook usage only. -// https://vitejs.dev/config/ -export default defineConfig({ - define: { - 'import.meta.env.VITE_RENDERER_VERSION': JSON.stringify(version ?? 'unspecified') - } -}); From e876a37c6af19e59f182d46482ab5f0a79bc91b3 Mon Sep 17 00:00:00 2001 From: Prosvirin Vladimir Date: Tue, 30 Sep 2025 16:04:30 +0300 Subject: [PATCH 8/8] Add factory and delete bug import bmiCalculator --- .github/workflows/build_test_lint.yml | 1 - .../sdc/BehaviorCalculations.stories.tsx | 43 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_test_lint.yml b/.github/workflows/build_test_lint.yml index ef1065150..1495888f5 100644 --- a/.github/workflows/build_test_lint.yml +++ b/.github/workflows/build_test_lint.yml @@ -153,7 +153,6 @@ jobs: - name: Build workspace packages run: | npm run build -w packages/sdc-populate - npm run build -w packages/testing-toolkit - name: Install Playwright browsers run: npx playwright install --with-deps diff --git a/packages/smart-forms-renderer/src/stories/sdc/BehaviorCalculations.stories.tsx b/packages/smart-forms-renderer/src/stories/sdc/BehaviorCalculations.stories.tsx index 46811f39f..a3320d323 100644 --- a/packages/smart-forms-renderer/src/stories/sdc/BehaviorCalculations.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/sdc/BehaviorCalculations.stories.tsx @@ -18,12 +18,16 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook'; import { - qCalculatedExpressionBMICalculator, qCalculatedExpressionCvdRiskCalculator, qInitialExpression, qLaunchContext, qVariable } from '../assets/questionnaires'; +import { + calculatedExpressionExtFactory, + questionnaireFactory, + variableExtFactory +} from '../testUtils'; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export const meta = { @@ -56,6 +60,43 @@ export const InitialExpression: Story = { } }; +const heightLinkId = 'patient-height'; +const weightLinkId = 'patient-weight'; +const bmiLinkIdCalc = 'bmi-result'; +const bmiGroupLinkId = 'bmi-calculation'; + +const qCalculatedExpressionBMICalculator = questionnaireFactory([ + { + linkId: bmiGroupLinkId, + type: 'group', + extension: [ + variableExtFactory('height', `item.where(linkId='${heightLinkId}').answer.value`), + variableExtFactory('weight', `item.where(linkId='${weightLinkId}').answer.value`) + ], + item: [ + { + linkId: heightLinkId, + text: 'Height', + type: 'decimal', + readOnly: false + }, + { + linkId: weightLinkId, + text: 'Weight', + type: 'decimal', + readOnly: false + }, + { + extension: [calculatedExpressionExtFactory('(%weight/((%height/100).power(2))).round(1)')], + linkId: bmiLinkIdCalc, + text: 'Value', + type: 'decimal', + readOnly: true + } + ] + } +]); + export const CalculatedExpressionBMICalculator: Story = { args: { questionnaire: qCalculatedExpressionBMICalculator