@fogpipe/forma-react 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +27 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/canProceed.test.ts +874 -0
- package/src/__tests__/diabetes-trial-flow.test.ts +776 -0
- package/src/__tests__/null-handling.test.ts +436 -0
- package/src/__tests__/test-utils.tsx +1 -0
- package/src/__tests__/useForma.test.ts +111 -0
- package/src/useForma.ts +38 -10
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the diabetes clinical trial enrollment form wizard flow
|
|
3
|
+
*
|
|
4
|
+
* This test suite documents expected wizard behavior for a complex multi-page
|
|
5
|
+
* form with conditional page visibility based on computed eligibility fields.
|
|
6
|
+
*
|
|
7
|
+
* The form has:
|
|
8
|
+
* - 10 pages with conditional visibility
|
|
9
|
+
* - Complex computed field dependency chains for eligibility determination
|
|
10
|
+
* - Conditional signature flows (participant vs LAR, optional witness)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from "vitest";
|
|
14
|
+
import { renderHook, act } from "@testing-library/react";
|
|
15
|
+
import { useForma } from "../useForma.js";
|
|
16
|
+
import type { Forma } from "@fogpipe/forma-core";
|
|
17
|
+
|
|
18
|
+
// Simplified version of the diabetes trial spec for focused testing
|
|
19
|
+
function createDiabetesTrialSpec(): Forma {
|
|
20
|
+
return {
|
|
21
|
+
version: "1.0",
|
|
22
|
+
meta: {
|
|
23
|
+
id: "diabetes-trial-enrollment",
|
|
24
|
+
title: "Diabetes Medication Clinical Trial Enrollment Form",
|
|
25
|
+
},
|
|
26
|
+
schema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
// Study info (pre-filled)
|
|
30
|
+
studyName: { type: "string" },
|
|
31
|
+
protocolNumber: { type: "string" },
|
|
32
|
+
// Participant info
|
|
33
|
+
screeningId: { type: "string" },
|
|
34
|
+
participantInitials: { type: "string" },
|
|
35
|
+
dateOfBirth: { type: "string", format: "date" },
|
|
36
|
+
// Inclusion criteria
|
|
37
|
+
inclusionAge: { type: "boolean" },
|
|
38
|
+
inclusionDiagnosis: { type: "boolean" },
|
|
39
|
+
inclusionVisits: { type: "boolean" },
|
|
40
|
+
inclusionConsent: { type: "boolean" },
|
|
41
|
+
// Exclusion criteria
|
|
42
|
+
exclusionPregnant: { type: "boolean" },
|
|
43
|
+
exclusionAllergy: { type: "boolean" },
|
|
44
|
+
exclusionRecentStudy: { type: "boolean" },
|
|
45
|
+
exclusionKidney: { type: "boolean" },
|
|
46
|
+
exclusionSglt2: { type: "boolean" },
|
|
47
|
+
// Ineligibility
|
|
48
|
+
ineligibilityReason: { type: "string" },
|
|
49
|
+
ineligibilityNotes: { type: "string" },
|
|
50
|
+
// Consents
|
|
51
|
+
consentPurpose: { type: "boolean" },
|
|
52
|
+
consentProcedures: { type: "boolean" },
|
|
53
|
+
consentRisks: { type: "boolean" },
|
|
54
|
+
consentBenefits: { type: "boolean" },
|
|
55
|
+
consentVoluntary: { type: "boolean" },
|
|
56
|
+
consentDataHandling: { type: "boolean" },
|
|
57
|
+
// Optional consents
|
|
58
|
+
optionalSamples: { type: "boolean" },
|
|
59
|
+
optionalContact: { type: "boolean" },
|
|
60
|
+
optionalGenetic: { type: "boolean" },
|
|
61
|
+
// Signatures
|
|
62
|
+
signingOnBehalf: { type: "boolean" },
|
|
63
|
+
participantSignatureName: { type: "string" },
|
|
64
|
+
participantSignatureDateTime: { type: "string" },
|
|
65
|
+
larName: { type: "string" },
|
|
66
|
+
larRelationship: { type: "string" },
|
|
67
|
+
larSignatureDateTime: { type: "string" },
|
|
68
|
+
researcherName: { type: "string" },
|
|
69
|
+
researcherConfirm: { type: "boolean" },
|
|
70
|
+
researcherSignatureDateTime: { type: "string" },
|
|
71
|
+
witnessRequired: { type: "boolean" },
|
|
72
|
+
witnessName: { type: "string" },
|
|
73
|
+
witnessSignatureDateTime: { type: "string" },
|
|
74
|
+
// Enrollment
|
|
75
|
+
enrollmentDate: { type: "string" },
|
|
76
|
+
randomizationNumber: { type: "string" },
|
|
77
|
+
treatmentGroup: { type: "string" },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
fields: {
|
|
81
|
+
studyName: { type: "text", label: "Study Name", enabledWhen: "false" },
|
|
82
|
+
protocolNumber: { type: "text", label: "Protocol Number", enabledWhen: "false" },
|
|
83
|
+
screeningId: { type: "text", label: "Screening ID" },
|
|
84
|
+
participantInitials: { type: "text", label: "Participant Initials" },
|
|
85
|
+
dateOfBirth: { type: "date", label: "Date of Birth" },
|
|
86
|
+
inclusionAge: { type: "boolean", label: "Age between 18-65 years" },
|
|
87
|
+
inclusionDiagnosis: { type: "boolean", label: "Confirmed Type 2 diabetes diagnosis" },
|
|
88
|
+
inclusionVisits: { type: "boolean", label: "Willing to attend all study visits" },
|
|
89
|
+
inclusionConsent: { type: "boolean", label: "Can give informed consent" },
|
|
90
|
+
exclusionPregnant: { type: "boolean", label: "Pregnant or planning to become pregnant" },
|
|
91
|
+
exclusionAllergy: { type: "boolean", label: "Allergy to metformin or similar drugs" },
|
|
92
|
+
exclusionRecentStudy: { type: "boolean", label: "Participated in another drug study in last 30 days" },
|
|
93
|
+
exclusionKidney: { type: "boolean", label: "History of severe kidney disease" },
|
|
94
|
+
exclusionSglt2: { type: "boolean", label: "Currently taking SGLT2 inhibitors" },
|
|
95
|
+
ineligibilityReason: { type: "textarea", label: "Ineligibility Reason", enabledWhen: "false" },
|
|
96
|
+
ineligibilityNotes: { type: "textarea", label: "Additional Notes" },
|
|
97
|
+
consentPurpose: { type: "boolean", label: "I have read and understand the study purpose" },
|
|
98
|
+
consentProcedures: { type: "boolean", label: "I understand the procedures I will undergo" },
|
|
99
|
+
consentRisks: { type: "boolean", label: "I understand the risks involved" },
|
|
100
|
+
consentBenefits: { type: "boolean", label: "I understand the potential benefits" },
|
|
101
|
+
consentVoluntary: { type: "boolean", label: "I understand participation is voluntary" },
|
|
102
|
+
consentDataHandling: { type: "boolean", label: "I understand how my data will be handled" },
|
|
103
|
+
optionalSamples: { type: "boolean", label: "I consent to storing blood samples" },
|
|
104
|
+
optionalContact: { type: "boolean", label: "I consent to being contacted about other studies" },
|
|
105
|
+
optionalGenetic: { type: "boolean", label: "I consent to genetic analysis" },
|
|
106
|
+
signingOnBehalf: { type: "boolean", label: "Is someone signing on behalf of the participant?" },
|
|
107
|
+
participantSignatureName: {
|
|
108
|
+
type: "text",
|
|
109
|
+
label: "Participant Signature Name",
|
|
110
|
+
visibleWhen: "signingOnBehalf = false",
|
|
111
|
+
requiredWhen: "signingOnBehalf = false",
|
|
112
|
+
},
|
|
113
|
+
participantSignatureDateTime: {
|
|
114
|
+
type: "datetime",
|
|
115
|
+
label: "Participant Signature Date and Time",
|
|
116
|
+
visibleWhen: "signingOnBehalf = false",
|
|
117
|
+
requiredWhen: "signingOnBehalf = false",
|
|
118
|
+
},
|
|
119
|
+
larName: {
|
|
120
|
+
type: "text",
|
|
121
|
+
label: "Legally Authorized Representative Name",
|
|
122
|
+
visibleWhen: "signingOnBehalf = true",
|
|
123
|
+
requiredWhen: "signingOnBehalf = true",
|
|
124
|
+
},
|
|
125
|
+
larRelationship: {
|
|
126
|
+
type: "text",
|
|
127
|
+
label: "Relationship to Participant",
|
|
128
|
+
visibleWhen: "signingOnBehalf = true",
|
|
129
|
+
requiredWhen: "signingOnBehalf = true",
|
|
130
|
+
},
|
|
131
|
+
larSignatureDateTime: {
|
|
132
|
+
type: "datetime",
|
|
133
|
+
label: "Representative Signature Date and Time",
|
|
134
|
+
visibleWhen: "signingOnBehalf = true",
|
|
135
|
+
requiredWhen: "signingOnBehalf = true",
|
|
136
|
+
},
|
|
137
|
+
researcherName: { type: "text", label: "Researcher Name" },
|
|
138
|
+
researcherConfirm: { type: "boolean", label: "I confirm I have properly explained the study" },
|
|
139
|
+
researcherSignatureDateTime: { type: "datetime", label: "Researcher Signature Date and Time" },
|
|
140
|
+
witnessRequired: { type: "boolean", label: "Is a witness signature required?" },
|
|
141
|
+
witnessName: {
|
|
142
|
+
type: "text",
|
|
143
|
+
label: "Witness Name",
|
|
144
|
+
visibleWhen: "witnessRequired = true",
|
|
145
|
+
requiredWhen: "witnessRequired = true",
|
|
146
|
+
},
|
|
147
|
+
witnessSignatureDateTime: {
|
|
148
|
+
type: "datetime",
|
|
149
|
+
label: "Witness Signature Date and Time",
|
|
150
|
+
visibleWhen: "witnessRequired = true",
|
|
151
|
+
requiredWhen: "witnessRequired = true",
|
|
152
|
+
},
|
|
153
|
+
enrollmentDate: { type: "date", label: "Enrollment Date" },
|
|
154
|
+
randomizationNumber: { type: "text", label: "Randomization Number" },
|
|
155
|
+
treatmentGroup: {
|
|
156
|
+
type: "select",
|
|
157
|
+
label: "Treatment Group",
|
|
158
|
+
options: [
|
|
159
|
+
{ value: "group-a", label: "Group A - Active Treatment" },
|
|
160
|
+
{ value: "group-b", label: "Group B - Placebo" },
|
|
161
|
+
{ value: "group-c", label: "Group C - Active Comparator" },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
fieldOrder: [
|
|
166
|
+
"studyName", "protocolNumber",
|
|
167
|
+
"screeningId", "participantInitials", "dateOfBirth",
|
|
168
|
+
"inclusionAge", "inclusionDiagnosis", "inclusionVisits", "inclusionConsent",
|
|
169
|
+
"exclusionPregnant", "exclusionAllergy", "exclusionRecentStudy", "exclusionKidney", "exclusionSglt2",
|
|
170
|
+
"ineligibilityReason", "ineligibilityNotes",
|
|
171
|
+
"consentPurpose", "consentProcedures", "consentRisks", "consentBenefits", "consentVoluntary", "consentDataHandling",
|
|
172
|
+
"optionalSamples", "optionalContact", "optionalGenetic",
|
|
173
|
+
"signingOnBehalf", "participantSignatureName", "participantSignatureDateTime",
|
|
174
|
+
"larName", "larRelationship", "larSignatureDateTime",
|
|
175
|
+
"researcherName", "researcherConfirm", "researcherSignatureDateTime",
|
|
176
|
+
"witnessRequired", "witnessName", "witnessSignatureDateTime",
|
|
177
|
+
"enrollmentDate", "randomizationNumber", "treatmentGroup",
|
|
178
|
+
],
|
|
179
|
+
computed: {
|
|
180
|
+
// Inclusion checks
|
|
181
|
+
allInclusionAnswered: {
|
|
182
|
+
expression: "(inclusionAge = true or inclusionAge = false) and (inclusionDiagnosis = true or inclusionDiagnosis = false) and (inclusionVisits = true or inclusionVisits = false) and (inclusionConsent = true or inclusionConsent = false)",
|
|
183
|
+
},
|
|
184
|
+
allInclusionMet: {
|
|
185
|
+
expression: "inclusionAge = true and inclusionDiagnosis = true and inclusionVisits = true and inclusionConsent = true",
|
|
186
|
+
},
|
|
187
|
+
// Exclusion checks
|
|
188
|
+
allExclusionAnswered: {
|
|
189
|
+
expression: "(exclusionPregnant = true or exclusionPregnant = false) and (exclusionAllergy = true or exclusionAllergy = false) and (exclusionRecentStudy = true or exclusionRecentStudy = false) and (exclusionKidney = true or exclusionKidney = false) and (exclusionSglt2 = true or exclusionSglt2 = false)",
|
|
190
|
+
},
|
|
191
|
+
anyExclusionMet: {
|
|
192
|
+
expression: "exclusionPregnant = true or exclusionAllergy = true or exclusionRecentStudy = true or exclusionKidney = true or exclusionSglt2 = true",
|
|
193
|
+
},
|
|
194
|
+
// Eligibility determination
|
|
195
|
+
eligibilityDetermined: {
|
|
196
|
+
expression: "computed.allInclusionAnswered = true and computed.allExclusionAnswered = true",
|
|
197
|
+
},
|
|
198
|
+
eligible: {
|
|
199
|
+
expression: "computed.eligibilityDetermined = true and computed.allInclusionMet = true and computed.anyExclusionMet = false",
|
|
200
|
+
},
|
|
201
|
+
ineligible: {
|
|
202
|
+
expression: "computed.eligibilityDetermined = true and (computed.allInclusionMet = false or computed.anyExclusionMet = true)",
|
|
203
|
+
},
|
|
204
|
+
// Consent checks
|
|
205
|
+
allMainConsentsSigned: {
|
|
206
|
+
expression: "consentPurpose = true and consentProcedures = true and consentRisks = true and consentBenefits = true and consentVoluntary = true and consentDataHandling = true",
|
|
207
|
+
},
|
|
208
|
+
// Signature checks - using null-safe patterns
|
|
209
|
+
hasParticipantSignature: {
|
|
210
|
+
expression: "signingOnBehalf != true and participantSignatureName != null and participantSignatureDateTime != null",
|
|
211
|
+
},
|
|
212
|
+
hasLarSignature: {
|
|
213
|
+
expression: "signingOnBehalf = true and larName != null and larRelationship != null and larSignatureDateTime != null",
|
|
214
|
+
},
|
|
215
|
+
hasValidSignature: {
|
|
216
|
+
expression: "computed.hasParticipantSignature = true or computed.hasLarSignature = true",
|
|
217
|
+
},
|
|
218
|
+
hasResearcherSignature: {
|
|
219
|
+
expression: "researcherName != null and researcherConfirm = true and researcherSignatureDateTime != null",
|
|
220
|
+
},
|
|
221
|
+
hasWitnessSignature: {
|
|
222
|
+
expression: "witnessRequired != true or (witnessName != null and witnessSignatureDateTime != null)",
|
|
223
|
+
},
|
|
224
|
+
allSignaturesComplete: {
|
|
225
|
+
expression: "computed.hasValidSignature = true and computed.hasResearcherSignature = true and computed.hasWitnessSignature = true",
|
|
226
|
+
},
|
|
227
|
+
// Final enrollment gate
|
|
228
|
+
canEnroll: {
|
|
229
|
+
expression: "computed.eligible = true and computed.allMainConsentsSigned = true and computed.allSignaturesComplete = true",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
pages: [
|
|
233
|
+
{
|
|
234
|
+
id: "study-information",
|
|
235
|
+
title: "Study Information",
|
|
236
|
+
fields: ["studyName", "protocolNumber"],
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: "participant-information",
|
|
240
|
+
title: "Participant Information",
|
|
241
|
+
fields: ["screeningId", "participantInitials", "dateOfBirth"],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: "inclusion-criteria",
|
|
245
|
+
title: "Eligibility Screening - Inclusion Criteria",
|
|
246
|
+
fields: ["inclusionAge", "inclusionDiagnosis", "inclusionVisits", "inclusionConsent"],
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: "exclusion-criteria",
|
|
250
|
+
title: "Eligibility Screening - Exclusion Criteria",
|
|
251
|
+
fields: ["exclusionPregnant", "exclusionAllergy", "exclusionRecentStudy", "exclusionKidney", "exclusionSglt2"],
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: "ineligibility-documentation",
|
|
255
|
+
title: "Ineligibility Documentation",
|
|
256
|
+
fields: ["ineligibilityReason", "ineligibilityNotes"],
|
|
257
|
+
visibleWhen: "computed.ineligible = true",
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: "main-consents",
|
|
261
|
+
title: "Informed Consent - Main Consents",
|
|
262
|
+
fields: ["consentPurpose", "consentProcedures", "consentRisks", "consentBenefits", "consentVoluntary", "consentDataHandling"],
|
|
263
|
+
visibleWhen: "computed.eligible = true",
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: "optional-consents",
|
|
267
|
+
title: "Optional Consents",
|
|
268
|
+
fields: ["optionalSamples", "optionalContact", "optionalGenetic"],
|
|
269
|
+
visibleWhen: "computed.eligible = true",
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "participant-signature",
|
|
273
|
+
title: "Participant Signature",
|
|
274
|
+
fields: ["signingOnBehalf", "participantSignatureName", "participantSignatureDateTime", "larName", "larRelationship", "larSignatureDateTime"],
|
|
275
|
+
visibleWhen: "computed.eligible = true and computed.allMainConsentsSigned = true",
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: "researcher-witness-signatures",
|
|
279
|
+
title: "Researcher and Witness Signatures",
|
|
280
|
+
fields: ["researcherName", "researcherConfirm", "researcherSignatureDateTime", "witnessRequired", "witnessName", "witnessSignatureDateTime"],
|
|
281
|
+
visibleWhen: "computed.eligible = true and computed.allMainConsentsSigned = true and computed.hasValidSignature = true",
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: "enrollment-details",
|
|
285
|
+
title: "Enrollment Details",
|
|
286
|
+
fields: ["enrollmentDate", "randomizationNumber", "treatmentGroup"],
|
|
287
|
+
visibleWhen: "computed.canEnroll = true",
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
describe("diabetes trial enrollment wizard", () => {
|
|
294
|
+
describe("initial page visibility", () => {
|
|
295
|
+
it("should show first 4 pages unconditionally", () => {
|
|
296
|
+
const spec = createDiabetesTrialSpec();
|
|
297
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
298
|
+
|
|
299
|
+
const pages = result.current.wizard?.pages;
|
|
300
|
+
expect(pages).toHaveLength(10);
|
|
301
|
+
|
|
302
|
+
// First 4 pages always visible
|
|
303
|
+
expect(pages?.[0].id).toBe("study-information");
|
|
304
|
+
expect(pages?.[0].visible).toBe(true);
|
|
305
|
+
|
|
306
|
+
expect(pages?.[1].id).toBe("participant-information");
|
|
307
|
+
expect(pages?.[1].visible).toBe(true);
|
|
308
|
+
|
|
309
|
+
expect(pages?.[2].id).toBe("inclusion-criteria");
|
|
310
|
+
expect(pages?.[2].visible).toBe(true);
|
|
311
|
+
|
|
312
|
+
expect(pages?.[3].id).toBe("exclusion-criteria");
|
|
313
|
+
expect(pages?.[3].visible).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("should hide conditional pages initially", () => {
|
|
317
|
+
const spec = createDiabetesTrialSpec();
|
|
318
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
319
|
+
|
|
320
|
+
const pages = result.current.wizard?.pages;
|
|
321
|
+
|
|
322
|
+
// Conditional pages hidden initially
|
|
323
|
+
expect(pages?.[4].id).toBe("ineligibility-documentation");
|
|
324
|
+
expect(pages?.[4].visible).toBe(false);
|
|
325
|
+
|
|
326
|
+
expect(pages?.[5].id).toBe("main-consents");
|
|
327
|
+
expect(pages?.[5].visible).toBe(false);
|
|
328
|
+
|
|
329
|
+
expect(pages?.[6].id).toBe("optional-consents");
|
|
330
|
+
expect(pages?.[6].visible).toBe(false);
|
|
331
|
+
|
|
332
|
+
expect(pages?.[7].id).toBe("participant-signature");
|
|
333
|
+
expect(pages?.[7].visible).toBe(false);
|
|
334
|
+
|
|
335
|
+
expect(pages?.[8].id).toBe("researcher-witness-signatures");
|
|
336
|
+
expect(pages?.[8].visible).toBe(false);
|
|
337
|
+
|
|
338
|
+
expect(pages?.[9].id).toBe("enrollment-details");
|
|
339
|
+
expect(pages?.[9].visible).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe("eligibility determination", () => {
|
|
344
|
+
it("should determine eligibility only after all criteria answered", () => {
|
|
345
|
+
const spec = createDiabetesTrialSpec();
|
|
346
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
347
|
+
|
|
348
|
+
// Initially not determined
|
|
349
|
+
expect(result.current.computed?.eligibilityDetermined).toBeFalsy();
|
|
350
|
+
|
|
351
|
+
// Fill inclusion criteria
|
|
352
|
+
act(() => {
|
|
353
|
+
result.current.setFieldValue("inclusionAge", true);
|
|
354
|
+
result.current.setFieldValue("inclusionDiagnosis", true);
|
|
355
|
+
result.current.setFieldValue("inclusionVisits", true);
|
|
356
|
+
result.current.setFieldValue("inclusionConsent", true);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Still not determined - exclusion not answered
|
|
360
|
+
expect(result.current.computed?.eligibilityDetermined).toBeFalsy();
|
|
361
|
+
|
|
362
|
+
// Fill exclusion criteria
|
|
363
|
+
act(() => {
|
|
364
|
+
result.current.setFieldValue("exclusionPregnant", false);
|
|
365
|
+
result.current.setFieldValue("exclusionAllergy", false);
|
|
366
|
+
result.current.setFieldValue("exclusionRecentStudy", false);
|
|
367
|
+
result.current.setFieldValue("exclusionKidney", false);
|
|
368
|
+
result.current.setFieldValue("exclusionSglt2", false);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Now determined
|
|
372
|
+
expect(result.current.computed?.eligibilityDetermined).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should mark eligible when all inclusion met and no exclusion met", () => {
|
|
376
|
+
const spec = createDiabetesTrialSpec();
|
|
377
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
378
|
+
|
|
379
|
+
// Fill all criteria - eligible scenario
|
|
380
|
+
act(() => {
|
|
381
|
+
// All inclusion = true
|
|
382
|
+
result.current.setFieldValue("inclusionAge", true);
|
|
383
|
+
result.current.setFieldValue("inclusionDiagnosis", true);
|
|
384
|
+
result.current.setFieldValue("inclusionVisits", true);
|
|
385
|
+
result.current.setFieldValue("inclusionConsent", true);
|
|
386
|
+
// All exclusion = false
|
|
387
|
+
result.current.setFieldValue("exclusionPregnant", false);
|
|
388
|
+
result.current.setFieldValue("exclusionAllergy", false);
|
|
389
|
+
result.current.setFieldValue("exclusionRecentStudy", false);
|
|
390
|
+
result.current.setFieldValue("exclusionKidney", false);
|
|
391
|
+
result.current.setFieldValue("exclusionSglt2", false);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(result.current.computed?.eligible).toBe(true);
|
|
395
|
+
expect(result.current.computed?.ineligible).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("should mark ineligible when any inclusion criterion is false", () => {
|
|
399
|
+
const spec = createDiabetesTrialSpec();
|
|
400
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
401
|
+
|
|
402
|
+
act(() => {
|
|
403
|
+
// One inclusion = false
|
|
404
|
+
result.current.setFieldValue("inclusionAge", false);
|
|
405
|
+
result.current.setFieldValue("inclusionDiagnosis", true);
|
|
406
|
+
result.current.setFieldValue("inclusionVisits", true);
|
|
407
|
+
result.current.setFieldValue("inclusionConsent", true);
|
|
408
|
+
// All exclusion = false
|
|
409
|
+
result.current.setFieldValue("exclusionPregnant", false);
|
|
410
|
+
result.current.setFieldValue("exclusionAllergy", false);
|
|
411
|
+
result.current.setFieldValue("exclusionRecentStudy", false);
|
|
412
|
+
result.current.setFieldValue("exclusionKidney", false);
|
|
413
|
+
result.current.setFieldValue("exclusionSglt2", false);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(result.current.computed?.eligible).toBe(false);
|
|
417
|
+
expect(result.current.computed?.ineligible).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("should mark ineligible when any exclusion criterion is true", () => {
|
|
421
|
+
const spec = createDiabetesTrialSpec();
|
|
422
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
423
|
+
|
|
424
|
+
act(() => {
|
|
425
|
+
// All inclusion = true
|
|
426
|
+
result.current.setFieldValue("inclusionAge", true);
|
|
427
|
+
result.current.setFieldValue("inclusionDiagnosis", true);
|
|
428
|
+
result.current.setFieldValue("inclusionVisits", true);
|
|
429
|
+
result.current.setFieldValue("inclusionConsent", true);
|
|
430
|
+
// One exclusion = true (pregnant)
|
|
431
|
+
result.current.setFieldValue("exclusionPregnant", true);
|
|
432
|
+
result.current.setFieldValue("exclusionAllergy", false);
|
|
433
|
+
result.current.setFieldValue("exclusionRecentStudy", false);
|
|
434
|
+
result.current.setFieldValue("exclusionKidney", false);
|
|
435
|
+
result.current.setFieldValue("exclusionSglt2", false);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
expect(result.current.computed?.eligible).toBe(false);
|
|
439
|
+
expect(result.current.computed?.ineligible).toBe(true);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe("eligible flow - page visibility", () => {
|
|
444
|
+
function setupEligibleParticipant(setFieldValue: (field: string, value: unknown) => void) {
|
|
445
|
+
// All inclusion = true
|
|
446
|
+
setFieldValue("inclusionAge", true);
|
|
447
|
+
setFieldValue("inclusionDiagnosis", true);
|
|
448
|
+
setFieldValue("inclusionVisits", true);
|
|
449
|
+
setFieldValue("inclusionConsent", true);
|
|
450
|
+
// All exclusion = false
|
|
451
|
+
setFieldValue("exclusionPregnant", false);
|
|
452
|
+
setFieldValue("exclusionAllergy", false);
|
|
453
|
+
setFieldValue("exclusionRecentStudy", false);
|
|
454
|
+
setFieldValue("exclusionKidney", false);
|
|
455
|
+
setFieldValue("exclusionSglt2", false);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
it("should show consent pages after eligibility confirmed", () => {
|
|
459
|
+
const spec = createDiabetesTrialSpec();
|
|
460
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
461
|
+
|
|
462
|
+
act(() => {
|
|
463
|
+
setupEligibleParticipant(result.current.setFieldValue);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const pages = result.current.wizard?.pages;
|
|
467
|
+
expect(pages?.[5].id).toBe("main-consents");
|
|
468
|
+
expect(pages?.[5].visible).toBe(true);
|
|
469
|
+
|
|
470
|
+
expect(pages?.[6].id).toBe("optional-consents");
|
|
471
|
+
expect(pages?.[6].visible).toBe(true);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("should show participant signature page after consents signed", () => {
|
|
475
|
+
const spec = createDiabetesTrialSpec();
|
|
476
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
477
|
+
|
|
478
|
+
act(() => {
|
|
479
|
+
setupEligibleParticipant(result.current.setFieldValue);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Signature page not yet visible
|
|
483
|
+
expect(result.current.wizard?.pages?.[7].visible).toBe(false);
|
|
484
|
+
|
|
485
|
+
// Sign all main consents
|
|
486
|
+
act(() => {
|
|
487
|
+
result.current.setFieldValue("consentPurpose", true);
|
|
488
|
+
result.current.setFieldValue("consentProcedures", true);
|
|
489
|
+
result.current.setFieldValue("consentRisks", true);
|
|
490
|
+
result.current.setFieldValue("consentBenefits", true);
|
|
491
|
+
result.current.setFieldValue("consentVoluntary", true);
|
|
492
|
+
result.current.setFieldValue("consentDataHandling", true);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
expect(result.current.computed?.allMainConsentsSigned).toBe(true);
|
|
496
|
+
expect(result.current.wizard?.pages?.[7].visible).toBe(true);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("should show researcher signature page after valid participant signature", () => {
|
|
500
|
+
const spec = createDiabetesTrialSpec();
|
|
501
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
502
|
+
|
|
503
|
+
act(() => {
|
|
504
|
+
setupEligibleParticipant(result.current.setFieldValue);
|
|
505
|
+
// Sign consents
|
|
506
|
+
result.current.setFieldValue("consentPurpose", true);
|
|
507
|
+
result.current.setFieldValue("consentProcedures", true);
|
|
508
|
+
result.current.setFieldValue("consentRisks", true);
|
|
509
|
+
result.current.setFieldValue("consentBenefits", true);
|
|
510
|
+
result.current.setFieldValue("consentVoluntary", true);
|
|
511
|
+
result.current.setFieldValue("consentDataHandling", true);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Researcher signature page not yet visible
|
|
515
|
+
expect(result.current.wizard?.pages?.[8].visible).toBe(false);
|
|
516
|
+
|
|
517
|
+
// Provide participant signature (not signing on behalf)
|
|
518
|
+
act(() => {
|
|
519
|
+
result.current.setFieldValue("signingOnBehalf", false);
|
|
520
|
+
result.current.setFieldValue("participantSignatureName", "John Doe");
|
|
521
|
+
result.current.setFieldValue("participantSignatureDateTime", "2024-01-15T10:00:00");
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
expect(result.current.computed?.hasValidSignature).toBe(true);
|
|
525
|
+
expect(result.current.wizard?.pages?.[8].visible).toBe(true);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("should show enrollment page only after all signatures complete", () => {
|
|
529
|
+
const spec = createDiabetesTrialSpec();
|
|
530
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
531
|
+
|
|
532
|
+
act(() => {
|
|
533
|
+
setupEligibleParticipant(result.current.setFieldValue);
|
|
534
|
+
// Sign consents
|
|
535
|
+
result.current.setFieldValue("consentPurpose", true);
|
|
536
|
+
result.current.setFieldValue("consentProcedures", true);
|
|
537
|
+
result.current.setFieldValue("consentRisks", true);
|
|
538
|
+
result.current.setFieldValue("consentBenefits", true);
|
|
539
|
+
result.current.setFieldValue("consentVoluntary", true);
|
|
540
|
+
result.current.setFieldValue("consentDataHandling", true);
|
|
541
|
+
// Participant signature
|
|
542
|
+
result.current.setFieldValue("signingOnBehalf", false);
|
|
543
|
+
result.current.setFieldValue("participantSignatureName", "John Doe");
|
|
544
|
+
result.current.setFieldValue("participantSignatureDateTime", "2024-01-15T10:00:00");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Enrollment page not yet visible
|
|
548
|
+
expect(result.current.wizard?.pages?.[9].visible).toBe(false);
|
|
549
|
+
|
|
550
|
+
// Add researcher signature
|
|
551
|
+
act(() => {
|
|
552
|
+
result.current.setFieldValue("researcherName", "Dr. Smith");
|
|
553
|
+
result.current.setFieldValue("researcherConfirm", true);
|
|
554
|
+
result.current.setFieldValue("researcherSignatureDateTime", "2024-01-15T10:30:00");
|
|
555
|
+
result.current.setFieldValue("witnessRequired", false);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
expect(result.current.computed?.canEnroll).toBe(true);
|
|
559
|
+
expect(result.current.wizard?.pages?.[9].visible).toBe(true);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
describe("ineligible flow - page visibility", () => {
|
|
564
|
+
it("should show ineligibility documentation when participant is ineligible", () => {
|
|
565
|
+
const spec = createDiabetesTrialSpec();
|
|
566
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
567
|
+
|
|
568
|
+
act(() => {
|
|
569
|
+
// One inclusion = false (not eligible)
|
|
570
|
+
result.current.setFieldValue("inclusionAge", false);
|
|
571
|
+
result.current.setFieldValue("inclusionDiagnosis", true);
|
|
572
|
+
result.current.setFieldValue("inclusionVisits", true);
|
|
573
|
+
result.current.setFieldValue("inclusionConsent", true);
|
|
574
|
+
// All exclusion answered
|
|
575
|
+
result.current.setFieldValue("exclusionPregnant", false);
|
|
576
|
+
result.current.setFieldValue("exclusionAllergy", false);
|
|
577
|
+
result.current.setFieldValue("exclusionRecentStudy", false);
|
|
578
|
+
result.current.setFieldValue("exclusionKidney", false);
|
|
579
|
+
result.current.setFieldValue("exclusionSglt2", false);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const pages = result.current.wizard?.pages;
|
|
583
|
+
|
|
584
|
+
// Ineligibility page visible
|
|
585
|
+
expect(pages?.[4].id).toBe("ineligibility-documentation");
|
|
586
|
+
expect(pages?.[4].visible).toBe(true);
|
|
587
|
+
|
|
588
|
+
// Consent pages hidden
|
|
589
|
+
expect(pages?.[5].visible).toBe(false);
|
|
590
|
+
expect(pages?.[6].visible).toBe(false);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("should hide eligible-only pages when ineligible", () => {
|
|
594
|
+
const spec = createDiabetesTrialSpec();
|
|
595
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
596
|
+
|
|
597
|
+
act(() => {
|
|
598
|
+
// All inclusion = true
|
|
599
|
+
result.current.setFieldValue("inclusionAge", true);
|
|
600
|
+
result.current.setFieldValue("inclusionDiagnosis", true);
|
|
601
|
+
result.current.setFieldValue("inclusionVisits", true);
|
|
602
|
+
result.current.setFieldValue("inclusionConsent", true);
|
|
603
|
+
// One exclusion = true (pregnant - ineligible)
|
|
604
|
+
result.current.setFieldValue("exclusionPregnant", true);
|
|
605
|
+
result.current.setFieldValue("exclusionAllergy", false);
|
|
606
|
+
result.current.setFieldValue("exclusionRecentStudy", false);
|
|
607
|
+
result.current.setFieldValue("exclusionKidney", false);
|
|
608
|
+
result.current.setFieldValue("exclusionSglt2", false);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
const pages = result.current.wizard?.pages;
|
|
612
|
+
|
|
613
|
+
// Ineligibility visible
|
|
614
|
+
expect(pages?.[4].visible).toBe(true);
|
|
615
|
+
|
|
616
|
+
// All eligible-only pages hidden
|
|
617
|
+
expect(pages?.[5].visible).toBe(false); // main-consents
|
|
618
|
+
expect(pages?.[6].visible).toBe(false); // optional-consents
|
|
619
|
+
expect(pages?.[7].visible).toBe(false); // participant-signature
|
|
620
|
+
expect(pages?.[8].visible).toBe(false); // researcher-witness-signatures
|
|
621
|
+
expect(pages?.[9].visible).toBe(false); // enrollment-details
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
describe("signature flow variations", () => {
|
|
626
|
+
function setupReadyForSignature(setFieldValue: (field: string, value: unknown) => void) {
|
|
627
|
+
// Eligible
|
|
628
|
+
setFieldValue("inclusionAge", true);
|
|
629
|
+
setFieldValue("inclusionDiagnosis", true);
|
|
630
|
+
setFieldValue("inclusionVisits", true);
|
|
631
|
+
setFieldValue("inclusionConsent", true);
|
|
632
|
+
setFieldValue("exclusionPregnant", false);
|
|
633
|
+
setFieldValue("exclusionAllergy", false);
|
|
634
|
+
setFieldValue("exclusionRecentStudy", false);
|
|
635
|
+
setFieldValue("exclusionKidney", false);
|
|
636
|
+
setFieldValue("exclusionSglt2", false);
|
|
637
|
+
// Consents signed
|
|
638
|
+
setFieldValue("consentPurpose", true);
|
|
639
|
+
setFieldValue("consentProcedures", true);
|
|
640
|
+
setFieldValue("consentRisks", true);
|
|
641
|
+
setFieldValue("consentBenefits", true);
|
|
642
|
+
setFieldValue("consentVoluntary", true);
|
|
643
|
+
setFieldValue("consentDataHandling", true);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
it("should show participant fields when not signing on behalf", () => {
|
|
647
|
+
const spec = createDiabetesTrialSpec();
|
|
648
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
649
|
+
|
|
650
|
+
act(() => {
|
|
651
|
+
setupReadyForSignature(result.current.setFieldValue);
|
|
652
|
+
result.current.setFieldValue("signingOnBehalf", false);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Participant fields visible
|
|
656
|
+
expect(result.current.visibility.participantSignatureName).toBe(true);
|
|
657
|
+
expect(result.current.visibility.participantSignatureDateTime).toBe(true);
|
|
658
|
+
|
|
659
|
+
// LAR fields hidden
|
|
660
|
+
expect(result.current.visibility.larName).toBe(false);
|
|
661
|
+
expect(result.current.visibility.larRelationship).toBe(false);
|
|
662
|
+
expect(result.current.visibility.larSignatureDateTime).toBe(false);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("should show LAR fields when signing on behalf", () => {
|
|
666
|
+
const spec = createDiabetesTrialSpec();
|
|
667
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
668
|
+
|
|
669
|
+
act(() => {
|
|
670
|
+
setupReadyForSignature(result.current.setFieldValue);
|
|
671
|
+
result.current.setFieldValue("signingOnBehalf", true);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// LAR fields visible
|
|
675
|
+
expect(result.current.visibility.larName).toBe(true);
|
|
676
|
+
expect(result.current.visibility.larRelationship).toBe(true);
|
|
677
|
+
expect(result.current.visibility.larSignatureDateTime).toBe(true);
|
|
678
|
+
|
|
679
|
+
// Participant fields hidden
|
|
680
|
+
expect(result.current.visibility.participantSignatureName).toBe(false);
|
|
681
|
+
expect(result.current.visibility.participantSignatureDateTime).toBe(false);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("should show witness fields when witness required", () => {
|
|
685
|
+
const spec = createDiabetesTrialSpec();
|
|
686
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
687
|
+
|
|
688
|
+
act(() => {
|
|
689
|
+
setupReadyForSignature(result.current.setFieldValue);
|
|
690
|
+
result.current.setFieldValue("witnessRequired", true);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
expect(result.current.visibility.witnessName).toBe(true);
|
|
694
|
+
expect(result.current.visibility.witnessSignatureDateTime).toBe(true);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("should hide witness fields when witness not required", () => {
|
|
698
|
+
const spec = createDiabetesTrialSpec();
|
|
699
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
700
|
+
|
|
701
|
+
act(() => {
|
|
702
|
+
setupReadyForSignature(result.current.setFieldValue);
|
|
703
|
+
result.current.setFieldValue("witnessRequired", false);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
expect(result.current.visibility.witnessName).toBe(false);
|
|
707
|
+
expect(result.current.visibility.witnessSignatureDateTime).toBe(false);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it("should accept LAR signature as valid signature", () => {
|
|
711
|
+
const spec = createDiabetesTrialSpec();
|
|
712
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
713
|
+
|
|
714
|
+
act(() => {
|
|
715
|
+
setupReadyForSignature(result.current.setFieldValue);
|
|
716
|
+
result.current.setFieldValue("signingOnBehalf", true);
|
|
717
|
+
result.current.setFieldValue("larName", "Jane Doe");
|
|
718
|
+
result.current.setFieldValue("larRelationship", "Spouse");
|
|
719
|
+
result.current.setFieldValue("larSignatureDateTime", "2024-01-15T10:00:00");
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
expect(result.current.computed?.hasLarSignature).toBe(true);
|
|
723
|
+
expect(result.current.computed?.hasValidSignature).toBe(true);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
describe("wizard navigation", () => {
|
|
728
|
+
it("should navigate through visible pages correctly", () => {
|
|
729
|
+
const spec = createDiabetesTrialSpec();
|
|
730
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
731
|
+
|
|
732
|
+
// Start on page 0
|
|
733
|
+
expect(result.current.wizard?.currentPageIndex).toBe(0);
|
|
734
|
+
expect(result.current.wizard?.currentPage?.id).toBe("study-information");
|
|
735
|
+
|
|
736
|
+
// Navigate forward
|
|
737
|
+
act(() => {
|
|
738
|
+
result.current.wizard?.nextPage();
|
|
739
|
+
});
|
|
740
|
+
expect(result.current.wizard?.currentPageIndex).toBe(1);
|
|
741
|
+
expect(result.current.wizard?.currentPage?.id).toBe("participant-information");
|
|
742
|
+
|
|
743
|
+
// Continue to inclusion
|
|
744
|
+
act(() => {
|
|
745
|
+
result.current.wizard?.nextPage();
|
|
746
|
+
});
|
|
747
|
+
expect(result.current.wizard?.currentPage?.id).toBe("inclusion-criteria");
|
|
748
|
+
|
|
749
|
+
// Continue to exclusion
|
|
750
|
+
act(() => {
|
|
751
|
+
result.current.wizard?.nextPage();
|
|
752
|
+
});
|
|
753
|
+
expect(result.current.wizard?.currentPage?.id).toBe("exclusion-criteria");
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it("should skip hidden pages during navigation", () => {
|
|
757
|
+
const spec = createDiabetesTrialSpec();
|
|
758
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
759
|
+
|
|
760
|
+
// Go to exclusion page (index 3)
|
|
761
|
+
act(() => {
|
|
762
|
+
result.current.wizard?.goToPage(3);
|
|
763
|
+
});
|
|
764
|
+
expect(result.current.wizard?.currentPage?.id).toBe("exclusion-criteria");
|
|
765
|
+
|
|
766
|
+
// Without eligibility determined, next visible page count is limited
|
|
767
|
+
// The visible pages should be: study-info (0), participant-info (1), inclusion (2), exclusion (3)
|
|
768
|
+
// Pages 4-9 are conditional and hidden
|
|
769
|
+
const visiblePages = result.current.wizard?.pages.filter(p => p.visible);
|
|
770
|
+
expect(visiblePages?.length).toBe(4);
|
|
771
|
+
|
|
772
|
+
// Should be on last visible page
|
|
773
|
+
expect(result.current.wizard?.isLastPage).toBe(true);
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
});
|