Compare commits
merge into: sina_sajjadi:master
sina_sajjadi:Dev
sina_sajjadi:front-test-2
sina_sajjadi:master
pull from: sina_sajjadi:Dev
sina_sajjadi:Dev
sina_sajjadi:front-test-2
sina_sajjadi:master
12 Commits
59 changed files with 2956 additions and 552 deletions
-
1api_questions.json
-
1api_questions_marital.json
-
234conditional-rules.js
-
77public/assets/images/diamond-color.svg
-
5src/app/api/proxy/route.ts
-
617src/app/new-match/new-match-client.tsx
-
88src/app/questions-list/[slug]/question-detail-client.tsx
-
58src/app/questions-list/questions-list-client.tsx
-
23src/app/questions-list/sections-request.tsx
-
49src/app/request-accepted/request-accepted-client.tsx
-
31src/app/request-sent/request-sent-client.tsx
-
48src/components/Componentes/advisor-actions-card.tsx
-
111src/components/Componentes/information-sheet.tsx
-
6src/components/Componentes/navigation-button.tsx
-
18src/components/Componentes/page-header.tsx
-
8src/components/Componentes/question-answer-storage.tsx
-
224src/components/Componentes/question-file.test.tsx
-
50src/components/Componentes/question-sheet.test.tsx
-
96src/components/Componentes/question-sheet.tsx
-
19src/components/Componentes/swipe-button.tsx
-
67src/components/Componentes/test-completed-sheet.test.tsx
-
61src/components/Componentes/test-completed-sheet.tsx
-
92src/components/Componentes/test-exit-sheet.test.tsx
-
78src/components/Componentes/test-exit-sheet.tsx
-
154src/components/Componentes/test-questions-flow.tsx
-
2src/hooks/marriage/use-form-schema.ts
-
25src/lib/auth-bridge.ts
-
639src/lib/conditional-rules.test.ts
-
41src/lib/conditional-rules.ts
-
66src/lib/schema-adapter.ts
-
7src/translations/locales/ar.json
-
7src/translations/locales/az.json
-
7src/translations/locales/bn.json
-
7src/translations/locales/da.json
-
7src/translations/locales/de.json
-
21src/translations/locales/en.json
-
7src/translations/locales/es.json
-
20src/translations/locales/fa.json
-
7src/translations/locales/fr.json
-
7src/translations/locales/gu.json
-
7src/translations/locales/ha.json
-
7src/translations/locales/he.json
-
7src/translations/locales/hi.json
-
7src/translations/locales/id.json
-
7src/translations/locales/ks.json
-
7src/translations/locales/pt.json
-
7src/translations/locales/ru.json
-
7src/translations/locales/sw.json
-
7src/translations/locales/tg.json
-
7src/translations/locales/tr.json
-
7src/translations/locales/ul.json
-
7src/translations/locales/ur.json
-
7src/translations/locales/uz.json
-
7src/translations/locales/zh.json
-
47test-all.js
-
29test-cond.js
-
32test-cond2.js
-
36test-cond3.js
-
26test-empty.js
1
api_questions.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1
api_questions_marital.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,234 @@ |
|||||
|
"use strict"; |
||||
|
Object.defineProperty(exports, "__esModule", { value: true }); |
||||
|
exports.canonicalRule = canonicalRule; |
||||
|
exports.isAnswerPresent = isAnswerPresent; |
||||
|
exports.matchesAudience = matchesAudience; |
||||
|
exports.ruleMatches = ruleMatches; |
||||
|
exports.isQuestionVisible = isQuestionVisible; |
||||
|
exports.isQuestionRequired = isQuestionRequired; |
||||
|
function canonicalRule(rule) { |
||||
|
if (!rule || typeof rule !== "object") { |
||||
|
return null; |
||||
|
} |
||||
|
// If wrapped in dependsOn (legacy)
|
||||
|
if (rule.dependsOn && !rule.parent_question_id) { |
||||
|
return { |
||||
|
dependsOn: rule.dependsOn, |
||||
|
}; |
||||
|
} |
||||
|
var operator = ["any_of", "all_of", "equals", "exists"].includes(rule.operator) |
||||
|
? rule.operator |
||||
|
: "any_of"; |
||||
|
var optionIds = []; |
||||
|
if (Array.isArray(rule.trigger_option_ids)) { |
||||
|
optionIds = rule.trigger_option_ids.map(String); |
||||
|
} |
||||
|
else if (rule.trigger_option_ids !== undefined && rule.trigger_option_ids !== null) { |
||||
|
optionIds = [String(rule.trigger_option_ids)]; |
||||
|
} |
||||
|
var result = { |
||||
|
parent_question_id: rule.parent_question_id || rule.parentQuestionId, |
||||
|
trigger_option_ids: optionIds, |
||||
|
operator: operator, |
||||
|
clear_answer_when_hidden: Boolean(rule.clear_answer_when_hidden), |
||||
|
}; |
||||
|
if (rule.audience && typeof rule.audience === "object") { |
||||
|
result.audience = rule.audience; |
||||
|
} |
||||
|
if (Array.isArray(rule.conditions)) { |
||||
|
result.conditions = rule.conditions |
||||
|
.map(canonicalRule) |
||||
|
.filter(function (c) { return c !== null; }); |
||||
|
result.conditions_operator = |
||||
|
rule.conditions_operator === "any_of" ? "any_of" : "all_of"; |
||||
|
result.root_operator = |
||||
|
rule.root_operator === "any_of" ? "any_of" : "all_of"; |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
function isAnswerPresent(answer) { |
||||
|
if (answer === undefined || answer === null) { |
||||
|
return false; |
||||
|
} |
||||
|
var val = typeof answer === "object" && "value" in answer ? answer.value : answer; |
||||
|
if (val === undefined || val === null) { |
||||
|
return false; |
||||
|
} |
||||
|
if (typeof val === "string") { |
||||
|
return val.trim().length > 0; |
||||
|
} |
||||
|
if (Array.isArray(val)) { |
||||
|
return val.length > 0; |
||||
|
} |
||||
|
if (typeof val === "object") { |
||||
|
return Object.keys(val).length > 0; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
function getSelectedOptionTokens(answer) { |
||||
|
var tokens = new Set(); |
||||
|
if (!answer) |
||||
|
return tokens; |
||||
|
var rawOptionId = typeof answer === "object" && "option_id" in answer |
||||
|
? answer.option_id |
||||
|
: undefined; |
||||
|
var rawValue = typeof answer === "object" && "value" in answer ? answer.value : answer; |
||||
|
var addToken = function (item) { |
||||
|
if (item === undefined || item === null) |
||||
|
return; |
||||
|
var str = String(item).trim(); |
||||
|
if (!str) |
||||
|
return; |
||||
|
tokens.add(str.toLowerCase()); |
||||
|
// If it has a dot prefix like "sec.q.opt", also add the suffix "opt"
|
||||
|
var lastDot = str.lastIndexOf("."); |
||||
|
if (lastDot !== -1 && lastDot < str.length - 1) { |
||||
|
tokens.add(str.slice(lastDot + 1).toLowerCase()); |
||||
|
} |
||||
|
}; |
||||
|
if (Array.isArray(rawOptionId)) { |
||||
|
rawOptionId.forEach(addToken); |
||||
|
} |
||||
|
else if (rawOptionId !== undefined) { |
||||
|
addToken(rawOptionId); |
||||
|
} |
||||
|
if (Array.isArray(rawValue)) { |
||||
|
rawValue.forEach(addToken); |
||||
|
} |
||||
|
else if (rawValue !== undefined) { |
||||
|
addToken(rawValue); |
||||
|
} |
||||
|
return tokens; |
||||
|
} |
||||
|
function matchesAudience(audience, context) { |
||||
|
if (!audience || typeof audience !== "object") { |
||||
|
return true; |
||||
|
} |
||||
|
if (audience.genders && audience.genders.length > 0) { |
||||
|
if (!context || !context.gender || !audience.genders.map(function(g) { return g.toLowerCase(); }).includes(context.gender.toLowerCase())) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
if (audience.minAge !== undefined && (context === null || context === void 0 ? void 0 : context.age) !== undefined && context.age !== null) { |
||||
|
if (context.age < audience.minAge) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
if (audience.maxAge !== undefined && (context === null || context === void 0 ? void 0 : context.age) !== undefined && context.age !== null) { |
||||
|
if (context.age > audience.maxAge) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
function ruleMatches(rawRule, answers, context) { |
||||
|
var rule = canonicalRule(rawRule); |
||||
|
if (!rule) { |
||||
|
return true; |
||||
|
} |
||||
|
if (rule.audience && !matchesAudience(rule.audience, context)) { |
||||
|
return false; |
||||
|
} |
||||
|
// Handle legacy dependsOn
|
||||
|
if (rule.dependsOn && rule.dependsOn.key) { |
||||
|
var parentId_1 = rule.dependsOn.key; |
||||
|
var answer = answers[parentId_1]; |
||||
|
if (!isAnswerPresent(answer)) { |
||||
|
return false; |
||||
|
} |
||||
|
var expectedValues = (rule.dependsOn.values || []).map(function (v) { |
||||
|
return String(v).toLowerCase().trim(); |
||||
|
}); |
||||
|
var actualTokens = getSelectedOptionTokens(answer); |
||||
|
var hasMatch = expectedValues.some(function (v) { return actualTokens.has(v); }); |
||||
|
return hasMatch; |
||||
|
} |
||||
|
var mainMatches = true; |
||||
|
var parentId = rule.parent_question_id; |
||||
|
if (parentId) { |
||||
|
var answer = answers[parentId]; |
||||
|
var operator = rule.operator || "any_of"; |
||||
|
if (operator === "exists") { |
||||
|
mainMatches = isAnswerPresent(answer); |
||||
|
} |
||||
|
else if (!isAnswerPresent(answer)) { |
||||
|
mainMatches = false; |
||||
|
} |
||||
|
else { |
||||
|
var actualTokens_1 = getSelectedOptionTokens(answer); |
||||
|
var expectedIds = (rule.trigger_option_ids || []).map(function (id) { |
||||
|
return String(id).toLowerCase().trim(); |
||||
|
}); |
||||
|
var isTokenMatched = function (expectedId) { |
||||
|
if (actualTokens_1.has(expectedId)) |
||||
|
return true; |
||||
|
var lastDot = expectedId.lastIndexOf("."); |
||||
|
if (lastDot !== -1 && lastDot < expectedId.length - 1) { |
||||
|
var suffix = expectedId.slice(lastDot + 1); |
||||
|
if (actualTokens_1.has(suffix)) |
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
|
}; |
||||
|
if (operator === "all_of") { |
||||
|
mainMatches = expectedIds.length > 0 && expectedIds.every(isTokenMatched); |
||||
|
} |
||||
|
else if (operator === "equals") { |
||||
|
mainMatches = |
||||
|
expectedIds.length > 0 && |
||||
|
expectedIds.every(isTokenMatched) && |
||||
|
actualTokens_1.size <= expectedIds.length * 2; |
||||
|
} |
||||
|
else { |
||||
|
// default: "any_of"
|
||||
|
mainMatches = expectedIds.some(isTokenMatched); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (rule.conditions && rule.conditions.length > 0) { |
||||
|
var subMatches = rule.conditions.map(function (cond) { |
||||
|
return ruleMatches(cond, answers, context); |
||||
|
}); |
||||
|
var condOperator = rule.conditions_operator || "all_of"; |
||||
|
var conditionsResult = condOperator === "any_of" |
||||
|
? subMatches.some(Boolean) |
||||
|
: subMatches.every(Boolean); |
||||
|
if (!parentId) { |
||||
|
return conditionsResult; |
||||
|
} |
||||
|
var rootOp = rule.root_operator || "all_of"; |
||||
|
return rootOp === "any_of" |
||||
|
? mainMatches || conditionsResult |
||||
|
: mainMatches && conditionsResult; |
||||
|
} |
||||
|
return parentId ? mainMatches : Boolean(!rule.audience || matchesAudience(rule.audience, context)); |
||||
|
} |
||||
|
function isQuestionVisible(question, answers, context) { |
||||
|
// 1. Audience check
|
||||
|
if (question.audience && !matchesAudience(question.audience, context)) { |
||||
|
return false; |
||||
|
} |
||||
|
// 2. Canonical visibility / conditional rule
|
||||
|
var rule = question.visibility || |
||||
|
question.conditionalRule || |
||||
|
question.logic; |
||||
|
if (rule) { |
||||
|
return ruleMatches(rule, answers, context); |
||||
|
} |
||||
|
if (question.isVisible !== undefined) { |
||||
|
return question.isVisible; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
function isQuestionRequired(question, answers, context) { |
||||
|
if (!isQuestionVisible(question, answers, context)) { |
||||
|
return false; |
||||
|
} |
||||
|
if (question.requiredWhen) { |
||||
|
if (question.requiredWhen.genders || question.requiredWhen.minAge !== undefined || question.requiredWhen.maxAge !== undefined) { |
||||
|
return matchesAudience(question.requiredWhen, context); |
||||
|
} |
||||
|
return ruleMatches(question.requiredWhen, answers, context); |
||||
|
} |
||||
|
return Boolean(question.baseRequired !== undefined ? question.baseRequired : question.required); |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
<svg width="64" height="64" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> |
||||
|
<defs> |
||||
|
<!-- Facet Gradients matching the exact vibrant spectrum --> |
||||
|
<linearGradient id="g_top_left" x1="6" y1="42" x2="22" y2="18" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0%" stop-color="#00C2FF"/> |
||||
|
<stop offset="100%" stop-color="#6366F1"/> |
||||
|
</linearGradient> |
||||
|
|
||||
|
<linearGradient id="g_top_mid_left" x1="22" y1="18" x2="32" y2="42" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0%" stop-color="#8B5CF6"/> |
||||
|
<stop offset="100%" stop-color="#D946EF"/> |
||||
|
</linearGradient> |
||||
|
|
||||
|
<linearGradient id="g_top_center" x1="32" y1="42" x2="68" y2="18" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0%" stop-color="#C084FC"/> |
||||
|
<stop offset="50%" stop-color="#F472B6"/> |
||||
|
<stop offset="100%" stop-color="#FDA4AF"/> |
||||
|
</linearGradient> |
||||
|
|
||||
|
<linearGradient id="g_top_mid_right" x1="50" y1="18" x2="78" y2="42" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0%" stop-color="#F43F5E"/> |
||||
|
<stop offset="100%" stop-color="#FB7185"/> |
||||
|
</linearGradient> |
||||
|
|
||||
|
<linearGradient id="g_top_right" x1="68" y1="42" x2="94" y2="42" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0%" stop-color="#FB7185"/> |
||||
|
<stop offset="100%" stop-color="#FB923C"/> |
||||
|
</linearGradient> |
||||
|
|
||||
|
<linearGradient id="g_bot_left" x1="6" y1="42" x2="50" y2="88" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0%" stop-color="#00BAFF"/> |
||||
|
<stop offset="60%" stop-color="#3B82F6"/> |
||||
|
<stop offset="100%" stop-color="#6366F1"/> |
||||
|
</linearGradient> |
||||
|
|
||||
|
<linearGradient id="g_bot_mid_left" x1="32" y1="42" x2="50" y2="88" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0%" stop-color="#6366F1"/> |
||||
|
<stop offset="50%" stop-color="#8B5CF6"/> |
||||
|
<stop offset="100%" stop-color="#A855F7"/> |
||||
|
</linearGradient> |
||||
|
|
||||
|
<linearGradient id="g_bot_mid_right" x1="50" y1="42" x2="50" y2="88" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0%" stop-color="#EC4899"/> |
||||
|
<stop offset="60%" stop-color="#D946EF"/> |
||||
|
<stop offset="100%" stop-color="#A855F7"/> |
||||
|
</linearGradient> |
||||
|
|
||||
|
<linearGradient id="g_bot_right" x1="94" y1="42" x2="50" y2="88" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0%" stop-color="#F43F5E"/> |
||||
|
<stop offset="50%" stop-color="#FB7185"/> |
||||
|
<stop offset="100%" stop-color="#FB923C"/> |
||||
|
</linearGradient> |
||||
|
|
||||
|
<filter id="subtle_glow" x="0" y="0" width="100" height="100" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
||||
|
<feGaussianBlur stdDeviation="1.5" result="blur"/> |
||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over"/> |
||||
|
</filter> |
||||
|
</defs> |
||||
|
|
||||
|
<g stroke="white" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"> |
||||
|
<!-- Top Row Facets --> |
||||
|
<polygon points="22,18 6,42 32,42" fill="url(#g_top_left)" /> |
||||
|
<polygon points="22,18 50,18 32,42" fill="url(#g_top_mid_left)" /> |
||||
|
<polygon points="32,42 50,18 68,42" fill="url(#g_top_center)" /> |
||||
|
<polygon points="50,18 78,18 68,42" fill="url(#g_top_mid_right)" /> |
||||
|
<polygon points="78,18 68,42 94,42" fill="url(#g_top_right)" /> |
||||
|
|
||||
|
<!-- Bottom Row Facets --> |
||||
|
<polygon points="6,42 32,42 50,88" fill="url(#g_bot_left)" /> |
||||
|
<polygon points="32,42 50,42 50,88" fill="url(#g_bot_mid_left)" /> |
||||
|
<polygon points="50,42 68,42 50,88" fill="url(#g_bot_mid_right)" /> |
||||
|
<polygon points="68,42 94,42 50,88" fill="url(#g_bot_right)" /> |
||||
|
</g> |
||||
|
|
||||
|
<!-- Outer highlight border for extra crispness --> |
||||
|
<polygon points="22,18 78,18 94,42 50,88 6,42" fill="none" stroke="white" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round" /> |
||||
|
</svg> |
||||
@ -0,0 +1,224 @@ |
|||||
|
import { |
||||
|
act, |
||||
|
cleanup, |
||||
|
fireEvent, |
||||
|
render, |
||||
|
screen, |
||||
|
waitFor, |
||||
|
} from "@testing-library/react"; |
||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
||||
|
import type { QuestionField } from "@/lib/schema-adapter"; |
||||
|
import { QuestionFile } from "./question-file"; |
||||
|
|
||||
|
let answerMap: Record<string, unknown> = {}; |
||||
|
const mockSetAnswerValue = vi.fn((q: QuestionField, val: unknown) => { |
||||
|
answerMap[q.id] = val; |
||||
|
}); |
||||
|
|
||||
|
const mockMutateAsync = vi.fn(async (file: File) => ({ |
||||
|
path: `/media/tmp/${file.name}`, |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/hooks/marriage/use-upload-tmp-media", () => ({ |
||||
|
useUploadTmpMediaMutation: () => ({ |
||||
|
mutate: vi.fn(), |
||||
|
mutateAsync: mockMutateAsync, |
||||
|
isPending: false, |
||||
|
isError: false, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/translations/provider", () => ({ |
||||
|
useI18n: () => ({ |
||||
|
locale: "en", |
||||
|
dictionary: { |
||||
|
upload_certificates: "upload certificates", |
||||
|
add_another_document: "Add another document", |
||||
|
max_files_reached: "Maximum of 4 files uploaded", |
||||
|
remove_document: "Remove document", |
||||
|
upload_failed: "Upload failed. Please try again.", |
||||
|
}, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("./question-answer-storage", () => ({ |
||||
|
useQuestionAnswers: () => ({ |
||||
|
getAnswerValue: (q: QuestionField) => answerMap[q.id] ?? null, |
||||
|
setAnswerValue: mockSetAnswerValue, |
||||
|
isLoading: false, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
const mockFileQuestion: QuestionField = { |
||||
|
id: "documents_verification.valid_identification_document", |
||||
|
title: "Valid Identification Document", |
||||
|
type: "file", |
||||
|
order: 1, |
||||
|
required: true, |
||||
|
baseRequired: true, |
||||
|
isVisible: true, |
||||
|
description: "Passport, National ID, or Driver's License", |
||||
|
tooltip: "", |
||||
|
extras: { |
||||
|
placeHolder: "Upload document", |
||||
|
range: [0, 0], |
||||
|
options: [".pdf", ".jpg", ".jpeg", ".png"], |
||||
|
}, |
||||
|
options: [], |
||||
|
}; |
||||
|
|
||||
|
describe("QuestionFile Component (Multiple Upload)", () => { |
||||
|
beforeEach(() => { |
||||
|
answerMap = {}; |
||||
|
mockSetAnswerValue.mockClear(); |
||||
|
mockMutateAsync.mockClear(); |
||||
|
vi.clearAllMocks(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
cleanup(); |
||||
|
}); |
||||
|
|
||||
|
it("renders empty dropzone when no files are uploaded without formats text", () => { |
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText(/Valid Identification/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/Passport, National ID, or Driver's License/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText("upload certificates")).toBeInTheDocument(); |
||||
|
expect(screen.queryByText("pdf, jpg, jpeg, png")).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("renders legacy single file stored as string correctly", () => { |
||||
|
answerMap[mockFileQuestion.id] = "/media/marriage/media/passport.pdf"; |
||||
|
|
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText("passport.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/Add another document/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/(1\/4)/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("renders multiple stored files correctly", () => { |
||||
|
answerMap[mockFileQuestion.id] = [ |
||||
|
"/media/marriage/media/doc1.pdf", |
||||
|
"/media/marriage/media/doc2.jpg", |
||||
|
"/media/marriage/media/doc3.png", |
||||
|
]; |
||||
|
|
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText("doc1.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByAltText("doc2.jpg")).toBeInTheDocument(); |
||||
|
expect(screen.getByAltText("doc3.png")).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/Add another document/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/(3\/4)/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("uploads a new file and transitions from empty state to list with Add button", async () => { |
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
const input = screen.getByLabelText("Upload files"); |
||||
|
expect(input).toBeInTheDocument(); |
||||
|
|
||||
|
const file = new File(["dummy content"], "national_id.pdf", { |
||||
|
type: "application/pdf", |
||||
|
}); |
||||
|
|
||||
|
await act(async () => { |
||||
|
fireEvent.change(input, { target: { files: [file] } }); |
||||
|
}); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(mockMutateAsync).toHaveBeenCalledWith(file); |
||||
|
expect(mockSetAnswerValue).toHaveBeenCalledWith(mockFileQuestion, [ |
||||
|
"/media/tmp/national_id.pdf", |
||||
|
]); |
||||
|
}); |
||||
|
|
||||
|
expect(screen.getByText("national_id.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/Add another document/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("allows adding 2nd, 3rd, 4th file and disables upload at 4 items", async () => { |
||||
|
answerMap[mockFileQuestion.id] = [ |
||||
|
"/media/marriage/media/file1.pdf", |
||||
|
"/media/marriage/media/file2.pdf", |
||||
|
"/media/marriage/media/file3.pdf", |
||||
|
]; |
||||
|
|
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText("file1.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("file2.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("file3.pdf")).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/(3\/4)/)).toBeInTheDocument(); |
||||
|
|
||||
|
// Add 4th file
|
||||
|
const addInput = screen.getByLabelText("Add file"); |
||||
|
const fourthFile = new File(["content"], "file4.jpg", { type: "image/jpeg" }); |
||||
|
|
||||
|
await act(async () => { |
||||
|
fireEvent.change(addInput, { target: { files: [fourthFile] } }); |
||||
|
}); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(mockSetAnswerValue).toHaveBeenCalledWith(mockFileQuestion, [ |
||||
|
"/media/marriage/media/file1.pdf", |
||||
|
"/media/marriage/media/file2.pdf", |
||||
|
"/media/marriage/media/file3.pdf", |
||||
|
"/media/tmp/file4.jpg", |
||||
|
]); |
||||
|
}); |
||||
|
|
||||
|
// Now 4 items are present, max files reached badge should be displayed
|
||||
|
expect(screen.getByText("Maximum of 4 files uploaded")).toBeInTheDocument(); |
||||
|
expect(screen.queryByText(/Add another document/)).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("deleting an item removes it from list, preserves remaining items, and restores Add button", async () => { |
||||
|
answerMap[mockFileQuestion.id] = [ |
||||
|
"/media/marriage/media/file1.pdf", |
||||
|
"/media/marriage/media/file2.jpg", |
||||
|
"/media/marriage/media/file3.png", |
||||
|
"/media/marriage/media/file4.pdf", |
||||
|
]; |
||||
|
|
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText("Maximum of 4 files uploaded")).toBeInTheDocument(); |
||||
|
|
||||
|
const deleteButtons = screen.getAllByTitle("Remove document"); |
||||
|
expect(deleteButtons).toHaveLength(4); |
||||
|
|
||||
|
// Delete 2nd file (file2.jpg)
|
||||
|
await act(async () => { |
||||
|
fireEvent.click(deleteButtons[1]); |
||||
|
}); |
||||
|
|
||||
|
expect(mockSetAnswerValue).toHaveBeenCalledWith(mockFileQuestion, [ |
||||
|
"/media/marriage/media/file1.pdf", |
||||
|
"/media/marriage/media/file3.png", |
||||
|
"/media/marriage/media/file4.pdf", |
||||
|
]); |
||||
|
|
||||
|
// Now 3 items remain, add button should be visible again
|
||||
|
expect(screen.getByText(/Add another document/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/(3\/4)/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("deleting the only item clears answer back to null and returns to empty dropzone", async () => { |
||||
|
answerMap[mockFileQuestion.id] = ["/media/marriage/media/only_file.pdf"]; |
||||
|
|
||||
|
render(<QuestionFile question={mockFileQuestion} />); |
||||
|
|
||||
|
expect(screen.getByText("only_file.pdf")).toBeInTheDocument(); |
||||
|
|
||||
|
const deleteButton = screen.getByTitle("Remove document"); |
||||
|
await act(async () => { |
||||
|
fireEvent.click(deleteButton); |
||||
|
}); |
||||
|
|
||||
|
expect(mockSetAnswerValue).toHaveBeenCalledWith(mockFileQuestion, null); |
||||
|
expect(screen.getByText("upload certificates")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,67 @@ |
|||||
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react"; |
||||
|
import { afterEach, describe, expect, it, vi } from "vitest"; |
||||
|
import { TestCompletedSheet } from "./test-completed-sheet"; |
||||
|
|
||||
|
vi.mock("@/translations/provider", () => ({ |
||||
|
useI18n: () => ({ |
||||
|
locale: "fa", |
||||
|
dictionary: {}, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/hooks/use-hardware-back-handler", () => ({ |
||||
|
useHardwareBackHandler: vi.fn(), |
||||
|
})); |
||||
|
|
||||
|
describe("TestCompletedSheet", () => { |
||||
|
afterEach(() => { |
||||
|
cleanup(); |
||||
|
}); |
||||
|
|
||||
|
it("renders completion message and 'متوجه شدم' button when open", () => { |
||||
|
render( |
||||
|
<TestCompletedSheet |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
title="تست گلاسر" |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByRole("heading", { name: "تست گلاسر" })).toBeInTheDocument(); |
||||
|
expect( |
||||
|
screen.getByText( |
||||
|
"شما این آزمون را قبلاً تکمیل کردهاید و امکان شرکت مجدد در آن وجود ندارد.", |
||||
|
), |
||||
|
).toBeInTheDocument(); |
||||
|
expect(screen.getByRole("button", { name: "متوجه شدم" })).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("calls onClose when clicking 'متوجه شدم' button", async () => { |
||||
|
const handleClose = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<TestCompletedSheet |
||||
|
isOpen={true} |
||||
|
onClose={handleClose} |
||||
|
title="تست شخصیتشناسی" |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
const gotItBtn = screen.getByRole("button", { name: "متوجه شدم" }); |
||||
|
fireEvent.click(gotItBtn); |
||||
|
|
||||
|
await new Promise((r) => setTimeout(r, 260)); |
||||
|
expect(handleClose).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("returns null when isOpen is false", () => { |
||||
|
const { container } = render( |
||||
|
<TestCompletedSheet |
||||
|
isOpen={false} |
||||
|
onClose={vi.fn()} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
expect(container.firstChild).toBeNull(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,61 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useI18n } from "@/translations/provider"; |
||||
|
import InformationSheet from "./information-sheet"; |
||||
|
import SwipeButton from "./swipe-button"; |
||||
|
|
||||
|
export type TestCompletedSheetProps = { |
||||
|
isOpen: boolean; |
||||
|
onClose: () => void; |
||||
|
title?: string; |
||||
|
closeOnOutside?: boolean; |
||||
|
}; |
||||
|
|
||||
|
export function TestCompletedSheet({ |
||||
|
isOpen, |
||||
|
onClose, |
||||
|
title, |
||||
|
closeOnOutside = true, |
||||
|
}: TestCompletedSheetProps) { |
||||
|
const { dictionary: t, locale } = useI18n(); |
||||
|
|
||||
|
if (!isOpen) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const isFa = locale === "fa"; |
||||
|
|
||||
|
const sheetTitle = |
||||
|
title || |
||||
|
(t as Record<string, string>)["Test Completed"] || |
||||
|
(isFa ? "آزمون تکمیل شده است" : "Test Completed"); |
||||
|
|
||||
|
const message = isFa |
||||
|
? "شما این آزمون را قبلاً تکمیل کردهاید و امکان شرکت مجدد در آن وجود ندارد." |
||||
|
: (t as Record<string, string>)["You have already completed this test, and it cannot be retaken."] || |
||||
|
"You have already completed this test, and it cannot be retaken."; |
||||
|
|
||||
|
const buttonLabel = (t as Record<string, string>)["Got it"] || (isFa ? "متوجه شدم" : "Got it"); |
||||
|
|
||||
|
return ( |
||||
|
<InformationSheet |
||||
|
icon="check" |
||||
|
title={sheetTitle} |
||||
|
description={ |
||||
|
<p className="text-center mt-2 group-12 text-[#4D4D4D] leading-relaxed font-medium"> |
||||
|
{message} |
||||
|
</p> |
||||
|
} |
||||
|
buttons={({ close }) => ( |
||||
|
<SwipeButton |
||||
|
text={buttonLabel} |
||||
|
onSuccess={close} |
||||
|
/> |
||||
|
)} |
||||
|
closeOnOutside={closeOnOutside} |
||||
|
onClose={onClose} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default TestCompletedSheet; |
||||
@ -0,0 +1,92 @@ |
|||||
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react"; |
||||
|
import { afterEach, describe, expect, it, vi } from "vitest"; |
||||
|
import { TestExitSheet } from "./test-exit-sheet"; |
||||
|
|
||||
|
vi.mock("@/translations/provider", () => ({ |
||||
|
useI18n: () => ({ |
||||
|
locale: "fa", |
||||
|
dictionary: {}, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/hooks/use-hardware-back-handler", () => ({ |
||||
|
useHardwareBackHandler: vi.fn(), |
||||
|
})); |
||||
|
|
||||
|
describe("TestExitSheet", () => { |
||||
|
afterEach(() => { |
||||
|
cleanup(); |
||||
|
}); |
||||
|
|
||||
|
it("renders warning title and explanation points when open", () => { |
||||
|
render( |
||||
|
<TestExitSheet |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onConfirmExit={vi.fn()} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByRole("heading", { name: "خروج از آزمون" })).toBeInTheDocument(); |
||||
|
expect( |
||||
|
screen.getByText("در صورت خروج از تست، ادامه فعلی حفظ نمیشود."), |
||||
|
).toBeInTheDocument(); |
||||
|
expect( |
||||
|
screen.getByText("پاسخهای واردشده ذخیره نخواهند شد."), |
||||
|
).toBeInTheDocument(); |
||||
|
expect( |
||||
|
screen.getByText("برای انجام دوباره تست باید از ابتدا شروع کنید."), |
||||
|
).toBeInTheDocument(); |
||||
|
expect(screen.getByText("ادامه آزمون")).toBeInTheDocument(); |
||||
|
expect(screen.getByRole("button", { name: "خروج از آزمون" })).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("calls onClose when clicking continue test (cancel)", async () => { |
||||
|
const handleClose = vi.fn(); |
||||
|
const handleConfirm = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<TestExitSheet |
||||
|
isOpen={true} |
||||
|
onClose={handleClose} |
||||
|
onConfirmExit={handleConfirm} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
const continueBtn = screen.getByText("ادامه آزمون"); |
||||
|
fireEvent.click(continueBtn); |
||||
|
|
||||
|
await new Promise((r) => setTimeout(r, 260)); |
||||
|
expect(handleClose).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("calls onConfirmExit when confirming exit", () => { |
||||
|
const handleClose = vi.fn(); |
||||
|
const handleConfirm = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<TestExitSheet |
||||
|
isOpen={true} |
||||
|
onClose={handleClose} |
||||
|
onConfirmExit={handleConfirm} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
const exitBtn = screen.getByRole("button", { name: "خروج از آزمون" }); |
||||
|
fireEvent.click(exitBtn); |
||||
|
|
||||
|
expect(handleConfirm).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("returns null when isOpen is false", () => { |
||||
|
const { container } = render( |
||||
|
<TestExitSheet |
||||
|
isOpen={false} |
||||
|
onClose={vi.fn()} |
||||
|
onConfirmExit={vi.fn()} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
expect(container.firstChild).toBeNull(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,78 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useI18n } from "@/translations/provider"; |
||||
|
import InformationSheet from "./information-sheet"; |
||||
|
import SwipeButton from "./swipe-button"; |
||||
|
|
||||
|
export type TestExitSheetProps = { |
||||
|
isOpen: boolean; |
||||
|
onClose: () => void; |
||||
|
onConfirmExit: () => void; |
||||
|
closeOnOutside?: boolean; |
||||
|
}; |
||||
|
|
||||
|
export function TestExitSheet({ |
||||
|
isOpen, |
||||
|
onClose, |
||||
|
onConfirmExit, |
||||
|
closeOnOutside = true, |
||||
|
}: TestExitSheetProps) { |
||||
|
const { dictionary: t, locale } = useI18n(); |
||||
|
|
||||
|
if (!isOpen) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const isFa = locale === "fa"; |
||||
|
|
||||
|
const tr = t as Record<string, string>; |
||||
|
const title = tr["Exit Test"] || (isFa ? "خروج از آزمون" : "Exit Test"); |
||||
|
|
||||
|
const points = isFa |
||||
|
? [ |
||||
|
"در صورت خروج از تست، ادامه فعلی حفظ نمیشود.", |
||||
|
"پاسخهای واردشده ذخیره نخواهند شد.", |
||||
|
"برای انجام دوباره تست باید از ابتدا شروع کنید.", |
||||
|
] |
||||
|
: [ |
||||
|
tr["If you exit the test, your current progress will not be saved."] || |
||||
|
"If you exit the test, your current progress will not be saved.", |
||||
|
tr["Your entered answers will not be saved."] || |
||||
|
"Your entered answers will not be saved.", |
||||
|
tr["To take the test again, you must start from the beginning."] || |
||||
|
"To take the test again, you must start from the beginning.", |
||||
|
]; |
||||
|
|
||||
|
const cancelLabel = |
||||
|
tr["Continue Test"] || (isFa ? "ادامه آزمون" : "Continue Test"); |
||||
|
const exitLabel = |
||||
|
tr["Exit Test"] || (isFa ? "خروج از آزمون" : "Exit Test"); |
||||
|
|
||||
|
return ( |
||||
|
<InformationSheet |
||||
|
icon="warning" |
||||
|
title={title} |
||||
|
description={ |
||||
|
<div className="flex flex-col gap-2 text-center mt-2 group-12 text-[#4D4D4D] leading-relaxed"> |
||||
|
{points.map((pt, idx) => ( |
||||
|
<p key={idx} className="font-medium"> |
||||
|
{pt} |
||||
|
</p> |
||||
|
))} |
||||
|
</div> |
||||
|
} |
||||
|
buttons={({ close }) => ( |
||||
|
<SwipeButton |
||||
|
text={exitLabel} |
||||
|
cancelText={cancelLabel} |
||||
|
onCancel={close} |
||||
|
onSuccess={onConfirmExit} |
||||
|
/> |
||||
|
)} |
||||
|
closeOnOutside={closeOnOutside} |
||||
|
onClose={onClose} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default TestExitSheet; |
||||
@ -0,0 +1,47 @@ |
|||||
|
const fs = require('fs'); |
||||
|
const rules = require('./conditional-rules.js'); |
||||
|
|
||||
|
const answers = { |
||||
|
"family_background.number_of_siblings": { |
||||
|
value: 2, |
||||
|
option_id: null |
||||
|
}, |
||||
|
"family_background.parents_survival_status": { |
||||
|
value: "both_parents_are_alive", |
||||
|
option_id: "family_background.parents_survival_status.both_parents_are_alive" |
||||
|
}, |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member": { |
||||
|
value: "i_am_responsible_for_caring_for_my_parent(s)_(father,_mother,_or_both).", |
||||
|
option_id: "family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_am_responsible_for_caring_for_my_parent_s_father_mother_or_both" |
||||
|
}, |
||||
|
"family_background.family_s_religious_and_ideological_atmosphere": { |
||||
|
value: "religious_(observant_of_obligations)", |
||||
|
option_id: "family_background.family_s_religious_and_ideological_atmosphere.religious_observant_of_obligations" |
||||
|
}, |
||||
|
"family_background.family_economic_status": { |
||||
|
value: "prosperous", |
||||
|
option_id: "family_background.family_economic_status.prosperous" |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const questions = JSON.parse(fs.readFileSync('api_questions.json', 'utf8')); |
||||
|
const context = { age: 25, gender: 'male' }; |
||||
|
|
||||
|
for (const q of questions) { |
||||
|
const mapped = { |
||||
|
id: q.id, |
||||
|
type: q.type, |
||||
|
title: q.title, |
||||
|
required: q.is_required !== undefined ? q.is_required : q.required, |
||||
|
baseRequired: q.required, |
||||
|
isVisible: q.is_visible, |
||||
|
conditionalRule: q.conditional_rule || q.visibility || q.logic || undefined, |
||||
|
visibility: q.visibility || q.conditional_rule || undefined, |
||||
|
logic: q.logic || undefined, |
||||
|
requiredWhen: q.required_when || undefined |
||||
|
}; |
||||
|
|
||||
|
const isVis = rules.isQuestionVisible(mapped, answers, context); |
||||
|
const isReq = rules.isQuestionRequired(mapped, answers, context); |
||||
|
console.log(`Q: ${q.id} | Visible: ${isVis} | Required: ${isReq}`); |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
const fs = require('fs'); |
||||
|
const ts = require('typescript'); |
||||
|
|
||||
|
// Compile conditional-rules.ts on the fly
|
||||
|
const source = fs.readFileSync('src/lib/conditional-rules.ts', 'utf8'); |
||||
|
const result = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS }}); |
||||
|
fs.writeFileSync('conditional-rules.js', result.outputText); |
||||
|
|
||||
|
const rules = require('./conditional-rules.js'); |
||||
|
|
||||
|
const answers = { |
||||
|
"family_background.parents_survival_status": { |
||||
|
value: "both_parents_are_alive", |
||||
|
option_id: "family_background.parents_survival_status.both_parents_are_alive" |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const visibility = { |
||||
|
operator: "any_of", |
||||
|
parent_question_id: "family_background.parents_survival_status", |
||||
|
trigger_option_ids: ["family_background.parents_survival_status.both_parents_are_alive"], |
||||
|
clear_answer_when_hidden: true |
||||
|
}; |
||||
|
|
||||
|
const context = { age: 25, gender: 'male' }; |
||||
|
|
||||
|
const isVisible = rules.ruleMatches(visibility, answers, context); |
||||
|
console.log("Is parents_marital_status visible?", isVisible); |
||||
|
|
||||
@ -0,0 +1,32 @@ |
|||||
|
const fs = require('fs'); |
||||
|
const rules = require('./conditional-rules.js'); |
||||
|
|
||||
|
const answers = { |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member": { |
||||
|
value: "i_am_responsible_for_caring_for_my_parent(s)_(father,_mother,_or_both).", |
||||
|
option_id: "family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_am_responsible_for_caring_for_my_parent_s_father_mother_or_both" |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const visibility = { |
||||
|
"operator": "any_of", |
||||
|
"parent_question_id": "family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member", |
||||
|
"trigger_option_ids": [ |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_am_responsible_for_caring_for_my_parent_s_father_mother_or_both", |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_am_responsible_for_the_care_custody_or_guardianship_of_other_family_members_sibling_etc", |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_regularly_provide_financial_support_for_a_family_member_s_living_expenses", |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_am_responsible_for_caring_for_my_father", |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_am_responsible_for_caring_for_my_mother", |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_am_responsible_for_caring_for_both_parents", |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_am_responsible_for_caring_for_a_sibling_brother_sister", |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_am_the_legal_guardian_or_supervisor_of_a_family_member", |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_have_other_circumstances_and_will_explain_in_the_description" |
||||
|
], |
||||
|
"clear_answer_when_hidden": true |
||||
|
}; |
||||
|
|
||||
|
const context = { age: 25, gender: 'male' }; |
||||
|
|
||||
|
const isVisible = rules.ruleMatches(visibility, answers, context); |
||||
|
console.log("Is additional_details visible?", isVisible); |
||||
|
|
||||
@ -0,0 +1,36 @@ |
|||||
|
const fs = require('fs'); |
||||
|
const rules = require('./conditional-rules.js'); |
||||
|
|
||||
|
const answers = { |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member": { |
||||
|
value: "i_am_responsible_for_caring_for_my_parent(s)_(father,_mother,_or_both).", |
||||
|
option_id: "family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_am_responsible_for_caring_for_my_parent_s_father_mother_or_both" |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const visibility = { |
||||
|
"operator": "any_of", |
||||
|
"parent_question_id": "family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member", |
||||
|
"trigger_option_ids": [ |
||||
|
"family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member.i_am_responsible_for_caring_for_my_parent_s_father_mother_or_both" |
||||
|
], |
||||
|
"clear_answer_when_hidden": true |
||||
|
}; |
||||
|
|
||||
|
const context = { age: 25, gender: 'male' }; |
||||
|
|
||||
|
const question = { |
||||
|
id: "family_background.additional_details_about_family_responsibility", |
||||
|
required: false, |
||||
|
baseRequired: false, // in backend DYNAMIC_REQUIRED sets is_required=True, but what does the schema send?
|
||||
|
isVisible: true, |
||||
|
conditionalRule: visibility, |
||||
|
visibility: visibility, |
||||
|
}; |
||||
|
|
||||
|
// Wait, the API sends `required: true` and `is_required: true` when it's dynamically required!
|
||||
|
// See check_form_section.py output: family_background.additional_details_about_family_responsibility - is_visible=True is_required=True
|
||||
|
question.required = true; |
||||
|
question.baseRequired = true; // Wait, schema adapter does baseRequired: bq.required, and required: bq.is_required !== undefined ? bq.is_required : bq.required
|
||||
|
|
||||
|
console.log("Is additional_details required?", rules.isQuestionRequired(question, answers, context)); |
||||
@ -0,0 +1,26 @@ |
|||||
|
const fs = require('fs'); |
||||
|
const rules = require('./conditional-rules.js'); |
||||
|
|
||||
|
const answers = {}; |
||||
|
|
||||
|
const questions = JSON.parse(fs.readFileSync('api_questions.json', 'utf8')); |
||||
|
const context = { age: 25, gender: 'male' }; |
||||
|
|
||||
|
for (const q of questions) { |
||||
|
const mapped = { |
||||
|
id: q.id, |
||||
|
type: q.type, |
||||
|
title: q.title, |
||||
|
required: q.is_required !== undefined ? q.is_required : q.required, |
||||
|
baseRequired: q.required, |
||||
|
isVisible: q.is_visible, |
||||
|
conditionalRule: q.conditional_rule || q.visibility || q.logic || undefined, |
||||
|
visibility: q.visibility || q.conditional_rule || undefined, |
||||
|
logic: q.logic || undefined, |
||||
|
requiredWhen: q.required_when || undefined |
||||
|
}; |
||||
|
|
||||
|
const isVis = rules.isQuestionVisible(mapped, answers, context); |
||||
|
const isReq = rules.isQuestionRequired(mapped, answers, context); |
||||
|
console.log(`Q: ${q.id} | Visible: ${isVis} | Required: ${isReq}`); |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue