mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-17 19:14:53 -05:00
addressed feedback
This commit is contained in:
@@ -1506,20 +1506,20 @@ checksums:
|
||||
environments/surveys/edit/upper_label: 1fa48bce3fade6ffc1a52d9fdddf9e17
|
||||
environments/surveys/edit/url_filters: e524879d2eb74463d7fd06a7e0f53421
|
||||
environments/surveys/edit/url_not_supported: af8a753467c617b596aadef1aaaed664
|
||||
environments/surveys/edit/validation/characters: f62970e214bd04fd1959e2759ee1ec48
|
||||
environments/surveys/edit/validation/contains: 06dd606c0a8f81f9a03b414e9ae89440
|
||||
environments/surveys/edit/validation/does_not_contain: 854da2bdf10613ce62fb454bab16c58b
|
||||
environments/surveys/edit/validation/characters: e26d6bb531181ec1ed551e264bc86259
|
||||
environments/surveys/edit/validation/contains: 41c8c25407527a5336404313f4c8d650
|
||||
environments/surveys/edit/validation/does_not_contain: d618eb0f854f7efa0d7c644e6628fa42
|
||||
environments/surveys/edit/validation/email: a481cd9fba3e145252458ee1eaa9bd3b
|
||||
environments/surveys/edit/validation/file_extension_is: c102e4962dd7b8b17faec31ecda6c9bd
|
||||
environments/surveys/edit/validation/file_extension_is_not: e5067a8ad6b89cd979651c9d8ee7c614
|
||||
environments/surveys/edit/validation/is: 6e18221b26a14df86537e89f50dcec16
|
||||
environments/surveys/edit/validation/is_between: 41ff45044d8a017a8a74f72be57916b8
|
||||
environments/surveys/edit/validation/is_earlier_than: cd82c79c61a8ad1d0e5b813d0a951860
|
||||
environments/surveys/edit/validation/is_greater_than: 314e7a053f9094fb4df21e1379d7f4f9
|
||||
environments/surveys/edit/validation/is_later_than: ffd9e3f745e85bc10fed450444787320
|
||||
environments/surveys/edit/validation/is_less_than: 5d497aca78681c793f32afd82fa85311
|
||||
environments/surveys/edit/validation/is_not: a59746bb7a7590c4dc5e57092a5fc138
|
||||
environments/surveys/edit/validation/is_not_between: c7e302092b76484d3fa1ca8c6b1ac609
|
||||
environments/surveys/edit/validation/is: 1940eeb4f6f0189788fde5403c6e9e9a
|
||||
environments/surveys/edit/validation/is_between: 5721c877c60f0005dc4ce78d4c0d3fdc
|
||||
environments/surveys/edit/validation/is_earlier_than: 3829d0a060cfc2c7f5f0281a55759612
|
||||
environments/surveys/edit/validation/is_greater_than: b9542ab0e0ea0ee18e82931b160b1385
|
||||
environments/surveys/edit/validation/is_later_than: 315eba60c6b8ca4cb3dd95c564ada456
|
||||
environments/surveys/edit/validation/is_less_than: 6109d595ba21497c59b1c91d7fd09a13
|
||||
environments/surveys/edit/validation/is_not: 8c7817ecdb08e6fa92fdf3487e0c8c9d
|
||||
environments/surveys/edit/validation/is_not_between: 4579a41b4e74d940eb036e13b3c63258
|
||||
environments/surveys/edit/validation/kb: 476c6cddd277e93a1bb7af4a763e95dc
|
||||
environments/surveys/edit/validation/max_length: 6edf9e1149c3893da102d9464138da22
|
||||
environments/surveys/edit/validation/max_selections: 6edf9e1149c3893da102d9464138da22
|
||||
@@ -1530,12 +1530,13 @@ checksums:
|
||||
environments/surveys/edit/validation/min_value: 204dbf1f1b3aa34c8b981642b1694262
|
||||
environments/surveys/edit/validation/minimum_options_ranked: 2dca1fb216c977a044987c65a0ca95c9
|
||||
environments/surveys/edit/validation/minimum_rows_answered: a8766a986cd73db0bb9daff49b271ed6
|
||||
environments/surveys/edit/validation/options_selected: 088309b017c07c01494447dba82b2621
|
||||
environments/surveys/edit/validation/options_selected: a7f72a7059a49a2a6d5b90f7a2a8aa44
|
||||
environments/surveys/edit/validation/pattern: c6f01d7bc9baa21a40ea38fa986bd5a0
|
||||
environments/surveys/edit/validation/phone: bcd7bd37a475ab1f80ea4c5b4d4d0bb5
|
||||
environments/surveys/edit/validation/rank_all_options: a885523e9d7820c9b0529bca37e48ccc
|
||||
environments/surveys/edit/validation/select_file_extensions: 208ccb7bd4dde20b0d79bdd1fa763076
|
||||
environments/surveys/edit/validation/url: 4006a4d8dfac013758f0053f6aa67cdd
|
||||
environments/surveys/edit/validation_logic_and: 05d02c5afac857da530b73dcf18dd8e4
|
||||
environments/surveys/edit/validation_logic_and: 83bb027b15e28b3dc1d6e16c7fc86056
|
||||
environments/surveys/edit/validation_logic_or: 32c9f3998984fd32a2b5bc53f2d97429
|
||||
environments/surveys/edit/validation_rules: 0cd99f02684d633196c8b249e857d207
|
||||
environments/surveys/edit/validation_rules_description: a0a7cee05e18efd462148698e3a93399
|
||||
|
||||
@@ -1591,9 +1591,9 @@
|
||||
"is_earlier_than": "ist früher als",
|
||||
"is_greater_than": "ist größer als",
|
||||
"is_later_than": "ist später als",
|
||||
"is_less_than": "ist kleiner als",
|
||||
"is_less_than": "ist weniger als",
|
||||
"is_not": "ist nicht",
|
||||
"is_not_between": "liegt nicht zwischen",
|
||||
"is_not_between": "ist nicht zwischen",
|
||||
"kb": "KB",
|
||||
"max_length": "Höchstens",
|
||||
"max_selections": "Höchstens",
|
||||
@@ -1607,10 +1607,11 @@
|
||||
"options_selected": "Optionen ausgewählt",
|
||||
"pattern": "Entspricht Regex-Muster",
|
||||
"phone": "Ist gültige Telefonnummer",
|
||||
"rank_all_options": "Alle Optionen bewerten",
|
||||
"select_file_extensions": "Dateierweiterungen auswählen...",
|
||||
"url": "Ist gültige URL"
|
||||
},
|
||||
"validation_logic_and": "alle sind wahr",
|
||||
"validation_logic_and": "Alle sind wahr",
|
||||
"validation_logic_or": "mindestens eine ist wahr",
|
||||
"validation_rules": "Validierungsregeln",
|
||||
"validation_rules_description": "Nur Antworten akzeptieren, die die folgenden Kriterien erfüllen",
|
||||
|
||||
@@ -1580,20 +1580,20 @@
|
||||
"url_filters": "URL Filters",
|
||||
"url_not_supported": "URL not supported",
|
||||
"validation": {
|
||||
"characters": "characters",
|
||||
"contains": "contains",
|
||||
"does_not_contain": "does not contain",
|
||||
"characters": "Characters",
|
||||
"contains": "Contains",
|
||||
"does_not_contain": "Does not contain",
|
||||
"email": "Is valid email",
|
||||
"file_extension_is": "File extension is",
|
||||
"file_extension_is_not": "File extension is not",
|
||||
"is": "is",
|
||||
"is_between": "is between",
|
||||
"is_earlier_than": "is earlier than",
|
||||
"is_greater_than": "is greater than",
|
||||
"is_later_than": "is later than",
|
||||
"is_less_than": "is less than",
|
||||
"is_not": "is not",
|
||||
"is_not_between": "is not between",
|
||||
"is": "Is",
|
||||
"is_between": "Is between",
|
||||
"is_earlier_than": "Is earlier than",
|
||||
"is_greater_than": "Is greater than",
|
||||
"is_later_than": "Is later than",
|
||||
"is_less_than": "Is less than",
|
||||
"is_not": "Is not",
|
||||
"is_not_between": "Is not between",
|
||||
"kb": "KB",
|
||||
"max_length": "At most",
|
||||
"max_selections": "At most",
|
||||
@@ -1604,13 +1604,14 @@
|
||||
"min_value": "At least",
|
||||
"minimum_options_ranked": "Minimum options ranked",
|
||||
"minimum_rows_answered": "Minimum rows answered",
|
||||
"options_selected": "options selected",
|
||||
"options_selected": "Options selected",
|
||||
"pattern": "Matches regex pattern",
|
||||
"phone": "Is valid phone",
|
||||
"rank_all_options": "Rank all options",
|
||||
"select_file_extensions": "Select file extensions...",
|
||||
"url": "Is valid URL"
|
||||
},
|
||||
"validation_logic_and": "all are true",
|
||||
"validation_logic_and": "All are true",
|
||||
"validation_logic_or": "any is true",
|
||||
"validation_rules": "Validation rules",
|
||||
"validation_rules_description": "Only accept responses that meet the following criteria",
|
||||
|
||||
@@ -1580,20 +1580,20 @@
|
||||
"url_filters": "Filtros de URL",
|
||||
"url_not_supported": "URL no compatible",
|
||||
"validation": {
|
||||
"characters": "caracteres",
|
||||
"contains": "contiene",
|
||||
"does_not_contain": "no contiene",
|
||||
"characters": "Caracteres",
|
||||
"contains": "Contiene",
|
||||
"does_not_contain": "No contiene",
|
||||
"email": "Es un correo electrónico válido",
|
||||
"file_extension_is": "La extensión del archivo es",
|
||||
"file_extension_is_not": "La extensión del archivo no es",
|
||||
"is": "es",
|
||||
"is_between": "está entre",
|
||||
"is_earlier_than": "es anterior a",
|
||||
"is_greater_than": "es mayor que",
|
||||
"is_later_than": "es posterior a",
|
||||
"is_less_than": "es menor que",
|
||||
"is_not": "no es",
|
||||
"is_not_between": "no está entre",
|
||||
"is": "Es",
|
||||
"is_between": "Está entre",
|
||||
"is_earlier_than": "Es anterior a",
|
||||
"is_greater_than": "Es mayor que",
|
||||
"is_later_than": "Es posterior a",
|
||||
"is_less_than": "Es menor que",
|
||||
"is_not": "No es",
|
||||
"is_not_between": "No está entre",
|
||||
"kb": "KB",
|
||||
"max_length": "Como máximo",
|
||||
"max_selections": "Como máximo",
|
||||
@@ -1604,13 +1604,14 @@
|
||||
"min_value": "Al menos",
|
||||
"minimum_options_ranked": "Opciones mínimas clasificadas",
|
||||
"minimum_rows_answered": "Filas mínimas respondidas",
|
||||
"options_selected": "opciones seleccionadas",
|
||||
"options_selected": "Opciones seleccionadas",
|
||||
"pattern": "Coincide con el patrón regex",
|
||||
"phone": "Es un teléfono válido",
|
||||
"rank_all_options": "Clasificar todas las opciones",
|
||||
"select_file_extensions": "Selecciona extensiones de archivo...",
|
||||
"url": "Es una URL válida"
|
||||
},
|
||||
"validation_logic_and": "todas son verdaderas",
|
||||
"validation_logic_and": "Todas son verdaderas",
|
||||
"validation_logic_or": "alguna es verdadera",
|
||||
"validation_rules": "Reglas de validación",
|
||||
"validation_rules_description": "Solo aceptar respuestas que cumplan los siguientes criterios",
|
||||
|
||||
@@ -1580,20 +1580,20 @@
|
||||
"url_filters": "Filtres d'URL",
|
||||
"url_not_supported": "URL non supportée",
|
||||
"validation": {
|
||||
"characters": "caractères",
|
||||
"contains": "contient",
|
||||
"does_not_contain": "ne contient pas",
|
||||
"characters": "Caractères",
|
||||
"contains": "Contient",
|
||||
"does_not_contain": "Ne contient pas",
|
||||
"email": "Est un e-mail valide",
|
||||
"file_extension_is": "L'extension de fichier est",
|
||||
"file_extension_is_not": "L'extension de fichier n'est pas",
|
||||
"is": "est",
|
||||
"is_between": "est entre",
|
||||
"is_earlier_than": "est antérieur à",
|
||||
"is_greater_than": "est supérieur à",
|
||||
"is_later_than": "est postérieur à",
|
||||
"is_less_than": "est inférieur à",
|
||||
"is_not": "n'est pas",
|
||||
"is_not_between": "n'est pas entre",
|
||||
"is": "Est",
|
||||
"is_between": "Est entre",
|
||||
"is_earlier_than": "Est antérieur à",
|
||||
"is_greater_than": "Est supérieur à",
|
||||
"is_later_than": "Est postérieur à",
|
||||
"is_less_than": "Est inférieur à",
|
||||
"is_not": "N'est pas",
|
||||
"is_not_between": "N'est pas entre",
|
||||
"kb": "Ko",
|
||||
"max_length": "Au maximum",
|
||||
"max_selections": "Au maximum",
|
||||
@@ -1604,13 +1604,14 @@
|
||||
"min_value": "Au moins",
|
||||
"minimum_options_ranked": "Nombre minimum d'options classées",
|
||||
"minimum_rows_answered": "Nombre minimum de lignes répondues",
|
||||
"options_selected": "options sélectionnées",
|
||||
"options_selected": "Options sélectionnées",
|
||||
"pattern": "Correspond au modèle d'expression régulière",
|
||||
"phone": "Est un numéro de téléphone valide",
|
||||
"rank_all_options": "Classer toutes les options",
|
||||
"select_file_extensions": "Sélectionner les extensions de fichier...",
|
||||
"url": "Est une URL valide"
|
||||
},
|
||||
"validation_logic_and": "toutes sont vraies",
|
||||
"validation_logic_and": "Toutes sont vraies",
|
||||
"validation_logic_or": "au moins une est vraie",
|
||||
"validation_rules": "Règles de validation",
|
||||
"validation_rules_description": "Accepter uniquement les réponses qui répondent aux critères suivants",
|
||||
|
||||
@@ -1580,7 +1580,7 @@
|
||||
"url_filters": "URLフィルター",
|
||||
"url_not_supported": "URLはサポートされていません",
|
||||
"validation": {
|
||||
"characters": "文字",
|
||||
"characters": "文字数",
|
||||
"contains": "を含む",
|
||||
"does_not_contain": "を含まない",
|
||||
"email": "有効なメールアドレスである",
|
||||
@@ -1593,7 +1593,7 @@
|
||||
"is_later_than": "より後である",
|
||||
"is_less_than": "より小さい",
|
||||
"is_not": "ではない",
|
||||
"is_not_between": "範囲外",
|
||||
"is_not_between": "の間ではない",
|
||||
"kb": "KB",
|
||||
"max_length": "最大",
|
||||
"max_selections": "最大",
|
||||
@@ -1604,13 +1604,14 @@
|
||||
"min_value": "最小",
|
||||
"minimum_options_ranked": "ランク付けされた最小オプション数",
|
||||
"minimum_rows_answered": "回答された最小行数",
|
||||
"options_selected": "個のオプションが選択されている",
|
||||
"options_selected": "選択されたオプション",
|
||||
"pattern": "正規表現パターンに一致する",
|
||||
"phone": "有効な電話番号である",
|
||||
"rank_all_options": "すべてのオプションをランク付け",
|
||||
"select_file_extensions": "ファイル拡張子を選択...",
|
||||
"url": "有効なURLである"
|
||||
},
|
||||
"validation_logic_and": "すべてが真",
|
||||
"validation_logic_and": "すべてが真である",
|
||||
"validation_logic_or": "いずれかが真",
|
||||
"validation_rules": "検証ルール",
|
||||
"validation_rules_description": "次の条件を満たす回答のみを受け付ける",
|
||||
|
||||
@@ -1580,20 +1580,20 @@
|
||||
"url_filters": "URL-filters",
|
||||
"url_not_supported": "URL niet ondersteund",
|
||||
"validation": {
|
||||
"characters": "tekens",
|
||||
"contains": "bevat",
|
||||
"does_not_contain": "bevat niet",
|
||||
"characters": "Tekens",
|
||||
"contains": "Bevat",
|
||||
"does_not_contain": "Bevat niet",
|
||||
"email": "Is geldig e-mailadres",
|
||||
"file_extension_is": "Bestandsextensie is",
|
||||
"file_extension_is_not": "Bestandsextensie is niet",
|
||||
"is": "is",
|
||||
"is_between": "is tussen",
|
||||
"is_earlier_than": "is eerder dan",
|
||||
"is_greater_than": "is groter dan",
|
||||
"is_later_than": "is later dan",
|
||||
"is_less_than": "is minder dan",
|
||||
"is_not": "is niet",
|
||||
"is_not_between": "ligt niet tussen",
|
||||
"is": "Is",
|
||||
"is_between": "Is tussen",
|
||||
"is_earlier_than": "Is eerder dan",
|
||||
"is_greater_than": "Is groter dan",
|
||||
"is_later_than": "Is later dan",
|
||||
"is_less_than": "Is minder dan",
|
||||
"is_not": "Is niet",
|
||||
"is_not_between": "Is niet tussen",
|
||||
"kb": "KB",
|
||||
"max_length": "Maximaal",
|
||||
"max_selections": "Maximaal",
|
||||
@@ -1604,13 +1604,14 @@
|
||||
"min_value": "Minimaal",
|
||||
"minimum_options_ranked": "Minimaal aantal gerangschikte opties",
|
||||
"minimum_rows_answered": "Minimaal aantal beantwoorde rijen",
|
||||
"options_selected": "opties geselecteerd",
|
||||
"options_selected": "Opties geselecteerd",
|
||||
"pattern": "Komt overeen met regex-patroon",
|
||||
"phone": "Is geldig telefoonnummer",
|
||||
"rank_all_options": "Rangschik alle opties",
|
||||
"select_file_extensions": "Selecteer bestandsextensies...",
|
||||
"url": "Is geldige URL"
|
||||
},
|
||||
"validation_logic_and": "alle zijn waar",
|
||||
"validation_logic_and": "Alle zijn waar",
|
||||
"validation_logic_or": "een is waar",
|
||||
"validation_rules": "Validatieregels",
|
||||
"validation_rules_description": "Accepteer alleen antwoorden die voldoen aan de volgende criteria",
|
||||
|
||||
@@ -1580,20 +1580,20 @@
|
||||
"url_filters": "Filtros de URL",
|
||||
"url_not_supported": "URL não suportada",
|
||||
"validation": {
|
||||
"characters": "caracteres",
|
||||
"contains": "contém",
|
||||
"does_not_contain": "não contém",
|
||||
"characters": "Caracteres",
|
||||
"contains": "Contém",
|
||||
"does_not_contain": "Não contém",
|
||||
"email": "É um e-mail válido",
|
||||
"file_extension_is": "A extensão do arquivo é",
|
||||
"file_extension_is_not": "A extensão do arquivo não é",
|
||||
"is": "é",
|
||||
"is_between": "está entre",
|
||||
"is_earlier_than": "é anterior a",
|
||||
"is_greater_than": "é maior que",
|
||||
"is_later_than": "é posterior a",
|
||||
"is_less_than": "é menor que",
|
||||
"is_not": "não é",
|
||||
"is_not_between": "não está entre",
|
||||
"is": "É",
|
||||
"is_between": "Está entre",
|
||||
"is_earlier_than": "É anterior a",
|
||||
"is_greater_than": "É maior que",
|
||||
"is_later_than": "É posterior a",
|
||||
"is_less_than": "É menor que",
|
||||
"is_not": "Não é",
|
||||
"is_not_between": "Não está entre",
|
||||
"kb": "KB",
|
||||
"max_length": "No máximo",
|
||||
"max_selections": "No máximo",
|
||||
@@ -1604,13 +1604,14 @@
|
||||
"min_value": "No mínimo",
|
||||
"minimum_options_ranked": "Mínimo de opções classificadas",
|
||||
"minimum_rows_answered": "Mínimo de linhas respondidas",
|
||||
"options_selected": "opções selecionadas",
|
||||
"options_selected": "Opções selecionadas",
|
||||
"pattern": "Corresponde ao padrão regex",
|
||||
"phone": "É um telefone válido",
|
||||
"rank_all_options": "Classificar todas as opções",
|
||||
"select_file_extensions": "Selecionar extensões de arquivo...",
|
||||
"url": "É uma URL válida"
|
||||
},
|
||||
"validation_logic_and": "todas são verdadeiras",
|
||||
"validation_logic_and": "Todas são verdadeiras",
|
||||
"validation_logic_or": "qualquer uma é verdadeira",
|
||||
"validation_rules": "Regras de validação",
|
||||
"validation_rules_description": "Aceitar apenas respostas que atendam aos seguintes critérios",
|
||||
|
||||
@@ -1580,20 +1580,20 @@
|
||||
"url_filters": "Filtros de URL",
|
||||
"url_not_supported": "URL não suportado",
|
||||
"validation": {
|
||||
"characters": "caracteres",
|
||||
"contains": "contém",
|
||||
"does_not_contain": "não contém",
|
||||
"characters": "Caracteres",
|
||||
"contains": "Contém",
|
||||
"does_not_contain": "Não contém",
|
||||
"email": "É um email válido",
|
||||
"file_extension_is": "A extensão do ficheiro é",
|
||||
"file_extension_is_not": "A extensão do ficheiro não é",
|
||||
"is": "é",
|
||||
"is_between": "está entre",
|
||||
"is_earlier_than": "é anterior a",
|
||||
"is_greater_than": "é maior que",
|
||||
"is_later_than": "é posterior a",
|
||||
"is_less_than": "é menor que",
|
||||
"is_not": "não é",
|
||||
"is_not_between": "não está entre",
|
||||
"is": "É",
|
||||
"is_between": "Está entre",
|
||||
"is_earlier_than": "É anterior a",
|
||||
"is_greater_than": "É maior que",
|
||||
"is_later_than": "É posterior a",
|
||||
"is_less_than": "É menor que",
|
||||
"is_not": "Não é",
|
||||
"is_not_between": "Não está entre",
|
||||
"kb": "KB",
|
||||
"max_length": "No máximo",
|
||||
"max_selections": "No máximo",
|
||||
@@ -1604,13 +1604,14 @@
|
||||
"min_value": "Pelo menos",
|
||||
"minimum_options_ranked": "Opções mínimas classificadas",
|
||||
"minimum_rows_answered": "Linhas mínimas respondidas",
|
||||
"options_selected": "opções selecionadas",
|
||||
"options_selected": "Opções selecionadas",
|
||||
"pattern": "Coincide com o padrão regex",
|
||||
"phone": "É um telefone válido",
|
||||
"rank_all_options": "Classificar todas as opções",
|
||||
"select_file_extensions": "Selecionar extensões de ficheiro...",
|
||||
"url": "É um URL válido"
|
||||
},
|
||||
"validation_logic_and": "todas são verdadeiras",
|
||||
"validation_logic_and": "Todas são verdadeiras",
|
||||
"validation_logic_or": "qualquer uma é verdadeira",
|
||||
"validation_rules": "Regras de validação",
|
||||
"validation_rules_description": "Aceitar apenas respostas que cumpram os seguintes critérios",
|
||||
|
||||
@@ -1580,20 +1580,20 @@
|
||||
"url_filters": "Filtre URL",
|
||||
"url_not_supported": "URL nesuportat",
|
||||
"validation": {
|
||||
"characters": "caractere",
|
||||
"contains": "conține",
|
||||
"does_not_contain": "nu conține",
|
||||
"characters": "Caractere",
|
||||
"contains": "Conține",
|
||||
"does_not_contain": "Nu conține",
|
||||
"email": "Este un email valid",
|
||||
"file_extension_is": "Extensia fișierului este",
|
||||
"file_extension_is_not": "Extensia fișierului nu este",
|
||||
"is": "este",
|
||||
"is_between": "este între",
|
||||
"is_earlier_than": "este mai devreme decât",
|
||||
"is_greater_than": "este mai mare decât",
|
||||
"is_later_than": "este mai târziu decât",
|
||||
"is_less_than": "este mai mic decât",
|
||||
"is_not": "nu este",
|
||||
"is_not_between": "nu este între",
|
||||
"is": "Este",
|
||||
"is_between": "Este între",
|
||||
"is_earlier_than": "Este mai devreme decât",
|
||||
"is_greater_than": "Este mai mare decât",
|
||||
"is_later_than": "Este mai târziu decât",
|
||||
"is_less_than": "Este mai mic decât",
|
||||
"is_not": "Nu este",
|
||||
"is_not_between": "Nu este între",
|
||||
"kb": "KB",
|
||||
"max_length": "Cel mult",
|
||||
"max_selections": "Cel mult",
|
||||
@@ -1604,13 +1604,14 @@
|
||||
"min_value": "Cel puțin",
|
||||
"minimum_options_ranked": "Număr minim de opțiuni ordonate",
|
||||
"minimum_rows_answered": "Număr minim de rânduri completate",
|
||||
"options_selected": "opțiuni selectate",
|
||||
"options_selected": "Opțiuni selectate",
|
||||
"pattern": "Se potrivește cu un șablon regex",
|
||||
"phone": "Este un număr de telefon valid",
|
||||
"rank_all_options": "Ordonați toate opțiunile",
|
||||
"select_file_extensions": "Selectați extensiile de fișier...",
|
||||
"url": "Este un URL valid"
|
||||
},
|
||||
"validation_logic_and": "toate sunt adevărate",
|
||||
"validation_logic_and": "Toate sunt adevărate",
|
||||
"validation_logic_or": "oricare este adevărată",
|
||||
"validation_rules": "Reguli de validare",
|
||||
"validation_rules_description": "Acceptă doar răspunsurile care îndeplinesc următoarele criterii",
|
||||
|
||||
@@ -1580,20 +1580,20 @@
|
||||
"url_filters": "Фильтры URL",
|
||||
"url_not_supported": "URL не поддерживается",
|
||||
"validation": {
|
||||
"characters": "символов",
|
||||
"contains": "содержит",
|
||||
"does_not_contain": "не содержит",
|
||||
"characters": "Символы",
|
||||
"contains": "Содержит",
|
||||
"does_not_contain": "Не содержит",
|
||||
"email": "Корректный email",
|
||||
"file_extension_is": "Расширение файла —",
|
||||
"file_extension_is_not": "Расширение файла не является",
|
||||
"is": "является",
|
||||
"is_between": "находится между",
|
||||
"is_earlier_than": "раньше чем",
|
||||
"is_greater_than": "больше чем",
|
||||
"is_later_than": "позже чем",
|
||||
"is_less_than": "меньше чем",
|
||||
"is_not": "не является",
|
||||
"is_not_between": "не находится между",
|
||||
"is": "Является",
|
||||
"is_between": "Находится между",
|
||||
"is_earlier_than": "Ранее чем",
|
||||
"is_greater_than": "Больше чем",
|
||||
"is_later_than": "Позже чем",
|
||||
"is_less_than": "Меньше чем",
|
||||
"is_not": "Не является",
|
||||
"is_not_between": "Не находится между",
|
||||
"kb": "КБ",
|
||||
"max_length": "Не более",
|
||||
"max_selections": "Не более",
|
||||
@@ -1604,13 +1604,14 @@
|
||||
"min_value": "Не менее",
|
||||
"minimum_options_ranked": "Минимальное количество ранжированных вариантов",
|
||||
"minimum_rows_answered": "Минимальное количество заполненных строк",
|
||||
"options_selected": "выбрано вариантов",
|
||||
"options_selected": "Выбранные опции",
|
||||
"pattern": "Соответствует шаблону regex",
|
||||
"phone": "Корректный телефон",
|
||||
"rank_all_options": "Ранжируйте все опции",
|
||||
"select_file_extensions": "Выберите расширения файлов...",
|
||||
"url": "Корректный URL"
|
||||
},
|
||||
"validation_logic_and": "все условия выполняются",
|
||||
"validation_logic_and": "Все условия выполняются",
|
||||
"validation_logic_or": "выполняется хотя бы одно условие",
|
||||
"validation_rules": "Правила валидации",
|
||||
"validation_rules_description": "Принимать только ответы, соответствующие следующим критериям",
|
||||
|
||||
@@ -1580,20 +1580,20 @@
|
||||
"url_filters": "URL-filter",
|
||||
"url_not_supported": "URL stöds inte",
|
||||
"validation": {
|
||||
"characters": "tecken",
|
||||
"contains": "innehåller",
|
||||
"does_not_contain": "innehåller inte",
|
||||
"characters": "Tecken",
|
||||
"contains": "Innehåller",
|
||||
"does_not_contain": "Innehåller inte",
|
||||
"email": "Är en giltig e-postadress",
|
||||
"file_extension_is": "Filändelsen är",
|
||||
"file_extension_is_not": "Filändelsen är inte",
|
||||
"is": "är",
|
||||
"is_between": "är mellan",
|
||||
"is_earlier_than": "är tidigare än",
|
||||
"is_greater_than": "är större än",
|
||||
"is_later_than": "är senare än",
|
||||
"is_less_than": "är mindre än",
|
||||
"is_not": "är inte",
|
||||
"is_not_between": "är inte mellan",
|
||||
"is": "Är",
|
||||
"is_between": "Är mellan",
|
||||
"is_earlier_than": "Är tidigare än",
|
||||
"is_greater_than": "Är större än",
|
||||
"is_later_than": "Är senare än",
|
||||
"is_less_than": "Är mindre än",
|
||||
"is_not": "Är inte",
|
||||
"is_not_between": "Är inte mellan",
|
||||
"kb": "KB",
|
||||
"max_length": "Högst",
|
||||
"max_selections": "Högst",
|
||||
@@ -1604,13 +1604,14 @@
|
||||
"min_value": "Minst",
|
||||
"minimum_options_ranked": "Minsta antal rangordnade alternativ",
|
||||
"minimum_rows_answered": "Minsta antal besvarade rader",
|
||||
"options_selected": "valda alternativ",
|
||||
"options_selected": "Valda alternativ",
|
||||
"pattern": "Matchar regexmönster",
|
||||
"phone": "Är ett giltigt telefonnummer",
|
||||
"rank_all_options": "Rangordna alla alternativ",
|
||||
"select_file_extensions": "Välj filändelser...",
|
||||
"url": "Är en giltig URL"
|
||||
},
|
||||
"validation_logic_and": "alla är sanna",
|
||||
"validation_logic_and": "Alla är sanna",
|
||||
"validation_logic_or": "någon är sann",
|
||||
"validation_rules": "Valideringsregler",
|
||||
"validation_rules_description": "Acceptera endast svar som uppfyller följande kriterier",
|
||||
|
||||
@@ -1580,20 +1580,20 @@
|
||||
"url_filters": "URL 过滤器",
|
||||
"url_not_supported": "URL 不支持",
|
||||
"validation": {
|
||||
"characters": "个字符",
|
||||
"characters": "字符",
|
||||
"contains": "包含",
|
||||
"does_not_contain": "不包含",
|
||||
"email": "是有效的邮箱地址",
|
||||
"file_extension_is": "文件扩展名为",
|
||||
"file_extension_is_not": "文件扩展名不是",
|
||||
"is": "是",
|
||||
"is": "等于",
|
||||
"is_between": "介于",
|
||||
"is_earlier_than": "早于",
|
||||
"is_greater_than": "大于",
|
||||
"is_later_than": "晚于",
|
||||
"is_less_than": "小于",
|
||||
"is_not": "不是",
|
||||
"is_not_between": "不在区间内",
|
||||
"is_not": "不等于",
|
||||
"is_not_between": "不介于",
|
||||
"kb": "KB",
|
||||
"max_length": "最多",
|
||||
"max_selections": "最多",
|
||||
@@ -1604,9 +1604,10 @@
|
||||
"min_value": "至少",
|
||||
"minimum_options_ranked": "最少排序选项数",
|
||||
"minimum_rows_answered": "最少回答行数",
|
||||
"options_selected": "项已选择",
|
||||
"options_selected": "已选择的选项",
|
||||
"pattern": "匹配正则表达式模式",
|
||||
"phone": "是有效的手机号",
|
||||
"rank_all_options": "对所有选项进行排序",
|
||||
"select_file_extensions": "选择文件扩展名...",
|
||||
"url": "是有效的URL"
|
||||
},
|
||||
|
||||
@@ -1580,20 +1580,20 @@
|
||||
"url_filters": "網址篩選器",
|
||||
"url_not_supported": "不支援網址",
|
||||
"validation": {
|
||||
"characters": "個字元",
|
||||
"characters": "字元",
|
||||
"contains": "包含",
|
||||
"does_not_contain": "不包含",
|
||||
"email": "是有效的電子郵件",
|
||||
"file_extension_is": "檔案副檔名為",
|
||||
"file_extension_is_not": "檔案副檔名不是",
|
||||
"is": "是",
|
||||
"is": "等於",
|
||||
"is_between": "介於",
|
||||
"is_earlier_than": "早於",
|
||||
"is_greater_than": "大於",
|
||||
"is_later_than": "晚於",
|
||||
"is_less_than": "小於",
|
||||
"is_not": "不是",
|
||||
"is_not_between": "不在範圍內",
|
||||
"is_not": "不等於",
|
||||
"is_not_between": "不介於",
|
||||
"kb": "KB",
|
||||
"max_length": "最多",
|
||||
"max_selections": "最多",
|
||||
@@ -1604,9 +1604,10 @@
|
||||
"min_value": "至少",
|
||||
"minimum_options_ranked": "最少排序選項數",
|
||||
"minimum_rows_answered": "最少作答列數",
|
||||
"options_selected": "個選項已選",
|
||||
"options_selected": "已選擇的選項",
|
||||
"pattern": "符合正則表達式樣式",
|
||||
"phone": "是有效的電話號碼",
|
||||
"rank_all_options": "請為所有選項排序",
|
||||
"select_file_extensions": "請選擇檔案副檔名...",
|
||||
"url": "是有效的 URL"
|
||||
},
|
||||
|
||||
@@ -112,6 +112,7 @@ export const EditorCardMenu = ({
|
||||
choices: card.choices,
|
||||
type,
|
||||
logic: undefined,
|
||||
validation: undefined,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -128,6 +129,7 @@ export const EditorCardMenu = ({
|
||||
buttonLabel,
|
||||
backButtonLabel,
|
||||
logic: undefined,
|
||||
validation: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -5,13 +5,12 @@ import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { type JSX } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyElementTypeEnum, TSurveyPictureSelectionElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyPictureSelectionElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { FileInput } from "@/modules/ui/components/file-input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
@@ -171,16 +170,6 @@ export const PictureSelectionForm = ({
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.PictureSelection}
|
||||
validation={element.validation}
|
||||
onUpdateValidation={(validation) => {
|
||||
updateElement(elementIdx, {
|
||||
validation,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -132,7 +132,6 @@ export const ValidationRuleRow = ({
|
||||
{config.unitOptions && config.unitOptions.length > 0 && (
|
||||
<ValidationRuleUnitSelector
|
||||
value={config.unitOptions[0].value}
|
||||
onChange={undefined}
|
||||
unitOptions={config.unitOptions}
|
||||
ruleLabels={ruleLabels}
|
||||
disabled={config.unitOptions.length === 1}
|
||||
|
||||
@@ -16,7 +16,6 @@ interface UnitOption {
|
||||
|
||||
interface ValidationRuleUnitSelectorProps {
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
unitOptions: UnitOption[];
|
||||
ruleLabels: Record<string, string>;
|
||||
disabled?: boolean;
|
||||
@@ -24,19 +23,18 @@ interface ValidationRuleUnitSelectorProps {
|
||||
|
||||
export const ValidationRuleUnitSelector = ({
|
||||
value,
|
||||
onChange,
|
||||
unitOptions,
|
||||
ruleLabels,
|
||||
disabled = false,
|
||||
}: ValidationRuleUnitSelectorProps) => {
|
||||
return (
|
||||
<Select value={value} onValueChange={onChange} disabled={disabled || unitOptions.length === 1}>
|
||||
<SelectTrigger className={cn("flex-1 bg-white", disabled && "cursor-not-allowed")}>
|
||||
<Select value={value} onValueChange={() => {}} disabled={disabled || unitOptions.length === 1}>
|
||||
<SelectTrigger className={cn("h-9 min-w-[180px] flex-1 bg-white", disabled && "cursor-not-allowed")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{unitOptions.map((unit) => (
|
||||
<SelectItem key={unit.value} value={unit.value}>
|
||||
<SelectItem key={unit.value} value={unit.value} className="truncate">
|
||||
{ruleLabels[unit.labelKey]}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import { TAllowedFileExtension } from "@formbricks/types/storage";
|
||||
@@ -48,8 +47,6 @@ export const ValidationRulesEditor = ({
|
||||
validation,
|
||||
onUpdateValidation,
|
||||
element,
|
||||
projectOrganizationId,
|
||||
isFormbricksCloud = false,
|
||||
inputType,
|
||||
onUpdateInputType,
|
||||
}: ValidationRulesEditorProps) => {
|
||||
@@ -216,7 +213,7 @@ export const ValidationRulesEditor = ({
|
||||
if (rule.id !== ruleId) return rule;
|
||||
const ruleType = rule.type;
|
||||
const config = RULE_TYPE_CONFIG[ruleType];
|
||||
const parsedValue = parseRuleValue(ruleType, value, config, rule.params);
|
||||
const parsedValue = parseRuleValue(ruleType, value, config);
|
||||
|
||||
return {
|
||||
...rule,
|
||||
|
||||
@@ -73,25 +73,25 @@ export const RULE_TYPE_CONFIG: Record<
|
||||
labelKey: "is",
|
||||
needsValue: true,
|
||||
valueType: "text",
|
||||
valuePlaceholder: "value",
|
||||
valuePlaceholder: "Value",
|
||||
},
|
||||
doesNotEqual: {
|
||||
labelKey: "is_not",
|
||||
needsValue: true,
|
||||
valueType: "text",
|
||||
valuePlaceholder: "value",
|
||||
valuePlaceholder: "Value",
|
||||
},
|
||||
contains: {
|
||||
labelKey: "contains",
|
||||
needsValue: true,
|
||||
valueType: "text",
|
||||
valuePlaceholder: "text",
|
||||
valuePlaceholder: "Text",
|
||||
},
|
||||
doesNotContain: {
|
||||
labelKey: "does_not_contain",
|
||||
needsValue: true,
|
||||
valueType: "text",
|
||||
valuePlaceholder: "text",
|
||||
valuePlaceholder: "Text",
|
||||
},
|
||||
isGreaterThan: {
|
||||
labelKey: "is_greater_than",
|
||||
@@ -135,6 +135,10 @@ export const RULE_TYPE_CONFIG: Record<
|
||||
valueType: "number",
|
||||
valuePlaceholder: "1",
|
||||
},
|
||||
rankAll: {
|
||||
labelKey: "rank_all_options",
|
||||
needsValue: false,
|
||||
},
|
||||
minRowsAnswered: {
|
||||
labelKey: "minimum_rows_answered",
|
||||
needsValue: true,
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import type {
|
||||
TSurveyElement,
|
||||
TSurveyMultipleChoiceElement,
|
||||
TSurveyRankingElement,
|
||||
} from "@formbricks/types/surveys/elements";
|
||||
import { RULE_TYPE_CONFIG } from "./validation-rules-config";
|
||||
import {
|
||||
getAddressFields,
|
||||
getContactInfoFields,
|
||||
getDefaultRuleValue,
|
||||
getRuleLabels,
|
||||
normalizeFileExtension,
|
||||
parseRuleValue,
|
||||
} from "./validation-rules-helpers";
|
||||
|
||||
// Mock translation function
|
||||
const mockT = (key: string): string => key;
|
||||
|
||||
describe("getAddressFields", () => {
|
||||
test("should return all address fields with correct labels", () => {
|
||||
const fields = getAddressFields(mockT);
|
||||
expect(fields).toHaveLength(6);
|
||||
expect(fields.map((f) => f.value)).toEqual([
|
||||
"addressLine1",
|
||||
"addressLine2",
|
||||
"city",
|
||||
"state",
|
||||
"zip",
|
||||
"country",
|
||||
]);
|
||||
expect(fields[0].label).toBe("environments.surveys.edit.address_line_1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getContactInfoFields", () => {
|
||||
test("should return all contact info fields with correct labels", () => {
|
||||
const fields = getContactInfoFields(mockT);
|
||||
expect(fields).toHaveLength(5);
|
||||
expect(fields.map((f) => f.value)).toEqual(["firstName", "lastName", "email", "phone", "company"]);
|
||||
expect(fields[0].label).toBe("environments.surveys.edit.first_name");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRuleLabels", () => {
|
||||
test("should return all rule labels", () => {
|
||||
const labels = getRuleLabels(mockT);
|
||||
expect(labels).toHaveProperty("min_length");
|
||||
expect(labels).toHaveProperty("max_length");
|
||||
expect(labels).toHaveProperty("pattern");
|
||||
expect(labels).toHaveProperty("email");
|
||||
expect(labels).toHaveProperty("url");
|
||||
expect(labels).toHaveProperty("phone");
|
||||
expect(labels).toHaveProperty("min_value");
|
||||
expect(labels).toHaveProperty("max_value");
|
||||
expect(labels).toHaveProperty("min_selections");
|
||||
expect(labels).toHaveProperty("max_selections");
|
||||
expect(labels).toHaveProperty("characters");
|
||||
expect(labels).toHaveProperty("options_selected");
|
||||
expect(labels).toHaveProperty("is");
|
||||
expect(labels).toHaveProperty("is_not");
|
||||
expect(labels).toHaveProperty("contains");
|
||||
expect(labels).toHaveProperty("does_not_contain");
|
||||
expect(labels).toHaveProperty("is_greater_than");
|
||||
expect(labels).toHaveProperty("is_less_than");
|
||||
expect(labels).toHaveProperty("is_later_than");
|
||||
expect(labels).toHaveProperty("is_earlier_than");
|
||||
expect(labels).toHaveProperty("is_between");
|
||||
expect(labels).toHaveProperty("is_not_between");
|
||||
expect(labels).toHaveProperty("minimum_options_ranked");
|
||||
expect(labels).toHaveProperty("rank_all_options");
|
||||
expect(labels).toHaveProperty("minimum_rows_answered");
|
||||
expect(labels).toHaveProperty("file_extension_is");
|
||||
expect(labels).toHaveProperty("file_extension_is_not");
|
||||
expect(labels).toHaveProperty("kb");
|
||||
expect(labels).toHaveProperty("mb");
|
||||
});
|
||||
|
||||
test("should return correct translation keys", () => {
|
||||
const labels = getRuleLabels(mockT);
|
||||
expect(labels.min_length).toBe("environments.surveys.edit.validation.min_length");
|
||||
expect(labels.email).toBe("environments.surveys.edit.validation.email");
|
||||
expect(labels.rank_all_options).toBe("environments.surveys.edit.validation.rank_all_options");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDefaultRuleValue", () => {
|
||||
test("should return undefined when config does not need value", () => {
|
||||
const config = RULE_TYPE_CONFIG.email;
|
||||
const value = getDefaultRuleValue(config);
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return empty string for text value type", () => {
|
||||
const config = RULE_TYPE_CONFIG.pattern;
|
||||
const value = getDefaultRuleValue(config);
|
||||
expect(value).toBe("");
|
||||
});
|
||||
|
||||
test("should return empty string for equals rule (has valueType: text, not option)", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "multi1",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
choices: [
|
||||
{ id: "opt1", label: { default: "Option 1" } },
|
||||
{ id: "opt2", label: { default: "Option 2" } },
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
],
|
||||
} as TSurveyMultipleChoiceElement;
|
||||
|
||||
const config = RULE_TYPE_CONFIG.equals;
|
||||
const value = getDefaultRuleValue(config, element);
|
||||
// equals has valueType: "text", not "option", so it returns "" (empty string for text type)
|
||||
expect(value).toBe("");
|
||||
});
|
||||
|
||||
test("should return empty string when config valueType is text (not option)", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "multi1",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
choices: [
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
{ id: "none", label: { default: "None" } },
|
||||
{ id: "opt1", label: { default: "Option 1" } },
|
||||
],
|
||||
} as TSurveyMultipleChoiceElement;
|
||||
|
||||
const config = RULE_TYPE_CONFIG.equals;
|
||||
const value = getDefaultRuleValue(config, element);
|
||||
// equals has valueType: "text", so it returns "" regardless of element choices
|
||||
expect(value).toBe("");
|
||||
});
|
||||
|
||||
test("should return empty string when no valid choices found for option value type", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "multi1",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
choices: [
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
{ id: "none", label: { default: "None" } },
|
||||
],
|
||||
} as TSurveyMultipleChoiceElement;
|
||||
|
||||
const config = RULE_TYPE_CONFIG.equals;
|
||||
const value = getDefaultRuleValue(config, element);
|
||||
expect(value).toBe("");
|
||||
});
|
||||
|
||||
test("should return empty string for option value type when element is not provided", () => {
|
||||
const config = RULE_TYPE_CONFIG.equals;
|
||||
const value = getDefaultRuleValue(config);
|
||||
expect(value).toBe("");
|
||||
});
|
||||
|
||||
test("should return undefined for number value type (minRanked uses number, not ranking)", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "rank1",
|
||||
type: TSurveyElementTypeEnum.Ranking,
|
||||
choices: [
|
||||
{ id: "opt1", label: { default: "Option 1" } },
|
||||
{ id: "opt2", label: { default: "Option 2" } },
|
||||
],
|
||||
} as TSurveyRankingElement;
|
||||
|
||||
const config = RULE_TYPE_CONFIG.minRanked;
|
||||
const value = getDefaultRuleValue(config, element);
|
||||
// minRanked has valueType: "number", not "ranking", so it returns undefined
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return undefined for number value type when element is not provided", () => {
|
||||
const config = RULE_TYPE_CONFIG.minRanked;
|
||||
const value = getDefaultRuleValue(config);
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeFileExtension", () => {
|
||||
test("should add dot prefix when missing", () => {
|
||||
expect(normalizeFileExtension("pdf")).toBe(".pdf");
|
||||
expect(normalizeFileExtension("jpg")).toBe(".jpg");
|
||||
});
|
||||
|
||||
test("should not add dot prefix when already present", () => {
|
||||
expect(normalizeFileExtension(".pdf")).toBe(".pdf");
|
||||
expect(normalizeFileExtension(".jpg")).toBe(".jpg");
|
||||
});
|
||||
|
||||
test("should handle empty string", () => {
|
||||
expect(normalizeFileExtension("")).toBe(".");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseRuleValue", () => {
|
||||
test("should normalize file extension for fileExtensionIs", () => {
|
||||
const config = RULE_TYPE_CONFIG.fileExtensionIs;
|
||||
const value = parseRuleValue("fileExtensionIs", "pdf", config);
|
||||
expect(value).toBe(".pdf");
|
||||
});
|
||||
|
||||
test("should normalize file extension for fileExtensionIsNot", () => {
|
||||
const config = RULE_TYPE_CONFIG.fileExtensionIsNot;
|
||||
const value = parseRuleValue("fileExtensionIsNot", "jpg", config);
|
||||
expect(value).toBe(".jpg");
|
||||
});
|
||||
|
||||
test("should not add dot if already present for file extension", () => {
|
||||
const config = RULE_TYPE_CONFIG.fileExtensionIs;
|
||||
const value = parseRuleValue("fileExtensionIs", ".pdf", config);
|
||||
expect(value).toBe(".pdf");
|
||||
});
|
||||
|
||||
test("should parse number for number value type", () => {
|
||||
const config = RULE_TYPE_CONFIG.minLength;
|
||||
const value = parseRuleValue("minLength", "10", config);
|
||||
expect(value).toBe(10);
|
||||
});
|
||||
|
||||
test("should return 0 for invalid number string", () => {
|
||||
const config = RULE_TYPE_CONFIG.minLength;
|
||||
const value = parseRuleValue("minLength", "invalid", config);
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
|
||||
test("should return string as-is for text value type", () => {
|
||||
const config = RULE_TYPE_CONFIG.pattern;
|
||||
const value = parseRuleValue("pattern", "test-pattern", config);
|
||||
expect(value).toBe("test-pattern");
|
||||
});
|
||||
|
||||
test("should return string as-is for equals rule", () => {
|
||||
const config = RULE_TYPE_CONFIG.equals;
|
||||
const value = parseRuleValue("equals", "test-value", config);
|
||||
expect(value).toBe("test-value");
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import {
|
||||
TAddressField,
|
||||
TContactInfoField,
|
||||
TValidationRule,
|
||||
TValidationRuleType,
|
||||
} from "@formbricks/types/surveys/validation-rules";
|
||||
import { RULE_TYPE_CONFIG } from "./validation-rules-config";
|
||||
@@ -53,6 +52,7 @@ export const getRuleLabels = (t: (key: string) => string): Record<string, string
|
||||
is_between: t("environments.surveys.edit.validation.is_between"),
|
||||
is_not_between: t("environments.surveys.edit.validation.is_not_between"),
|
||||
minimum_options_ranked: t("environments.surveys.edit.validation.minimum_options_ranked"),
|
||||
rank_all_options: t("environments.surveys.edit.validation.rank_all_options"),
|
||||
minimum_rows_answered: t("environments.surveys.edit.validation.minimum_rows_answered"),
|
||||
file_extension_is: t("environments.surveys.edit.validation.file_extension_is"),
|
||||
file_extension_is_not: t("environments.surveys.edit.validation.file_extension_is_not"),
|
||||
@@ -101,8 +101,7 @@ export const normalizeFileExtension = (value: string): string => {
|
||||
export const parseRuleValue = (
|
||||
ruleType: TValidationRuleType,
|
||||
value: string,
|
||||
config: (typeof RULE_TYPE_CONFIG)[TValidationRuleType],
|
||||
currentParams: TValidationRule["params"]
|
||||
config: (typeof RULE_TYPE_CONFIG)[TValidationRuleType]
|
||||
): string | number => {
|
||||
// Handle file extension formatting: auto-add dot if missing
|
||||
if (ruleType === "fileExtensionIs" || ruleType === "fileExtensionIsNot") {
|
||||
|
||||
@@ -138,7 +138,8 @@ describe("getAvailableRuleTypes", () => {
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toContain("minRanked");
|
||||
expect(available.length).toBe(1);
|
||||
expect(available).toContain("rankAll");
|
||||
expect(available.length).toBe(2);
|
||||
});
|
||||
|
||||
test("should return file validation rules for fileUpload element", () => {
|
||||
@@ -151,15 +152,13 @@ describe("getAvailableRuleTypes", () => {
|
||||
expect(available).toContain("fileExtensionIsNot");
|
||||
});
|
||||
|
||||
test("should return minSelections and maxSelections for pictureSelection element", () => {
|
||||
test("should return empty array for pictureSelection element (no validation rules)", () => {
|
||||
const elementType = TSurveyElementTypeEnum.PictureSelection;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toContain("minSelections");
|
||||
expect(available).toContain("maxSelections");
|
||||
expect(available.length).toBe(2);
|
||||
expect(available).toEqual([]);
|
||||
});
|
||||
|
||||
test("should return empty array for address element (no validation rules)", () => {
|
||||
|
||||
@@ -221,6 +221,8 @@ export const createRuleParams = (
|
||||
return { max: Number(value) || 3 };
|
||||
case "minRanked":
|
||||
return { min: Number(value) || 1 };
|
||||
case "rankAll":
|
||||
return {};
|
||||
case "minRowsAnswered":
|
||||
return { min: Number(value) || 1 };
|
||||
case "fileExtensionIs":
|
||||
|
||||
@@ -39,24 +39,59 @@ export function MultiSelect<T extends string, K extends TOption<T>["value"][]>(
|
||||
const [position, setPosition] = React.useState<{ top: number; left: number; width: number } | null>(null);
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Track if changes are user-initiated (not from value prop)
|
||||
const isUserInitiatedRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (value) {
|
||||
setSelected(
|
||||
value.map((val) => options.find((o) => o.value === val)).filter((o): o is TOption<T> => !!o)
|
||||
);
|
||||
const newSelected = value
|
||||
.map((val) => options.find((o) => o.value === val))
|
||||
.filter((o): o is TOption<T> => !!o);
|
||||
// Only update if different (avoid unnecessary updates)
|
||||
const currentValues = selected.map((s) => s.value);
|
||||
const newValues = newSelected.map((s) => s.value);
|
||||
if (
|
||||
currentValues.length !== newValues.length ||
|
||||
currentValues.some((val, idx) => val !== newValues[idx])
|
||||
) {
|
||||
isUserInitiatedRef.current = false; // Mark as prop-initiated
|
||||
setSelected(newSelected);
|
||||
}
|
||||
}
|
||||
}, [value, options]);
|
||||
|
||||
// Sync user-initiated selected changes to parent via onChange (deferred to avoid render issues)
|
||||
const prevSelectedRef = React.useRef(selected);
|
||||
React.useEffect(() => {
|
||||
// Only call onChange if change was user-initiated and selected actually changed
|
||||
if (isUserInitiatedRef.current && prevSelectedRef.current !== selected) {
|
||||
const selectedValues = selected.map((s) => s.value) as K;
|
||||
const prevValues = prevSelectedRef.current.map((s) => s.value) as K;
|
||||
// Check if values actually changed
|
||||
if (
|
||||
selectedValues.length !== prevValues.length ||
|
||||
selectedValues.some((val, idx) => val !== prevValues[idx])
|
||||
) {
|
||||
// Use queueMicrotask to defer the onChange call after render
|
||||
queueMicrotask(() => {
|
||||
onChange?.(selectedValues);
|
||||
});
|
||||
}
|
||||
prevSelectedRef.current = selected;
|
||||
isUserInitiatedRef.current = false; // Reset flag
|
||||
} else if (!isUserInitiatedRef.current) {
|
||||
// Update ref even if not user-initiated to track state
|
||||
prevSelectedRef.current = selected;
|
||||
}
|
||||
}, [selected, onChange]);
|
||||
|
||||
const handleUnselect = React.useCallback(
|
||||
(option: TOption<T>) => {
|
||||
if (disabled) return;
|
||||
setSelected((prev) => {
|
||||
const newSelected = prev.filter((s) => s.value !== option.value);
|
||||
onChange?.(newSelected.map((s) => s.value) as K);
|
||||
return newSelected;
|
||||
});
|
||||
isUserInitiatedRef.current = true; // Mark as user-initiated
|
||||
setSelected((prev) => prev.filter((s) => s.value !== option.value));
|
||||
},
|
||||
[onChange, disabled]
|
||||
[disabled]
|
||||
);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
@@ -65,10 +100,10 @@ export function MultiSelect<T extends string, K extends TOption<T>["value"][]>(
|
||||
if (!input || disabled) return;
|
||||
|
||||
if ((e.key === "Delete" || e.key === "Backspace") && input.value === "") {
|
||||
isUserInitiatedRef.current = true; // Mark as user-initiated
|
||||
setSelected((prev) => {
|
||||
const newSelected = [...prev];
|
||||
newSelected.pop();
|
||||
onChange?.(newSelected.map((s) => s.value) as K);
|
||||
return newSelected;
|
||||
});
|
||||
}
|
||||
@@ -109,8 +144,9 @@ export function MultiSelect<T extends string, K extends TOption<T>["value"][]>(
|
||||
className={`relative overflow-visible bg-white ${disabled ? "cursor-not-allowed opacity-50" : ""}`}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`border-input ring-offset-background group rounded-md border px-3 py-2 text-sm focus-within:ring-2 focus-within:ring-offset-2 ${disabled ? "pointer-events-none" : "focus-within:ring-ring"
|
||||
}`}>
|
||||
className={`border-input ring-offset-background group rounded-md border px-3 py-2 text-sm focus-within:ring-2 focus-within:ring-offset-2 ${
|
||||
disabled ? "pointer-events-none" : "focus-within:ring-ring"
|
||||
}`}>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selected.map((option) => (
|
||||
<Badge key={option.value} className="rounded-md">
|
||||
@@ -168,9 +204,8 @@ export function MultiSelect<T extends string, K extends TOption<T>["value"][]>(
|
||||
}}
|
||||
onSelect={() => {
|
||||
if (disabled) return;
|
||||
const newSelected = [...selected, option];
|
||||
setSelected(newSelected);
|
||||
onChange?.(newSelected.map((o) => o.value) as K);
|
||||
isUserInitiatedRef.current = true; // Mark as user-initiated
|
||||
setSelected((prev) => [...prev, option]);
|
||||
setInputValue("");
|
||||
}}
|
||||
className="cursor-pointer">
|
||||
|
||||
@@ -30,6 +30,7 @@ checksums:
|
||||
common/the_servers_cannot_be_reached_at_the_moment: f8adbeccac69f9230a55b5b3af52b081
|
||||
common/they_will_be_redirected_immediately: 936bc99cb575cba95ea8f04d82bb353b
|
||||
common/your_feedback_is_stuck: db2b6aba26723b01aee0fc918d3ca052
|
||||
errors/all_options_must_be_ranked: 360a2edff623496f7047907bad115ea1
|
||||
errors/file_extension_must_be: 3102dc81a482f1b05ee490767b1c3c97
|
||||
errors/file_extension_must_not_be: bd21065a4201d9a29b126586aecd8f29
|
||||
errors/file_input/duplicate_files: 198dd29e67beb6abc5b2534ede7d7f68
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "تعليقك عالق :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "يرجى ترتيب جميع الخيارات",
|
||||
"file_extension_must_be": "يجب أن يكون امتداد الملف {extension}",
|
||||
"file_extension_must_not_be": "يجب ألا يكون امتداد الملف {extension}",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "Ihr Feedback steckt fest :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "Bitte ordnen Sie alle Optionen ein",
|
||||
"file_extension_must_be": "Die Dateierweiterung muss {extension} sein",
|
||||
"file_extension_must_not_be": "Die Dateierweiterung darf nicht {extension} sein",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "Your feedback is stuck :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "Please rank all options",
|
||||
"file_extension_must_be": "File extension must be {extension}",
|
||||
"file_extension_must_not_be": "File extension must not be {extension}",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "Tu feedback está atascado :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "Por favor, clasifica todas las opciones",
|
||||
"file_extension_must_be": "La extensión del archivo debe ser {extension}",
|
||||
"file_extension_must_not_be": "La extensión del archivo no debe ser {extension}",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "Votre feedback est bloqué :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "Veuillez classer toutes les options",
|
||||
"file_extension_must_be": "L'extension du fichier doit être {extension}",
|
||||
"file_extension_must_not_be": "L'extension du fichier ne doit pas être {extension}",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "आपकी प्रतिक्रिया अटक गई है :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "कृपया सभी विकल्पों को रैंक करें",
|
||||
"file_extension_must_be": "फ़ाइल एक्सटेंशन {extension} होना चाहिए",
|
||||
"file_extension_must_not_be": "फ़ाइल एक्सटेंशन {extension} नहीं होना चाहिए",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "Il tuo feedback è bloccato :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "Classifica tutte le opzioni",
|
||||
"file_extension_must_be": "L'estensione del file deve essere {extension}",
|
||||
"file_extension_must_not_be": "L'estensione del file non deve essere {extension}",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "フィードバックが送信できません :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "すべてのオプションをランク付けしてください",
|
||||
"file_extension_must_be": "ファイル拡張子は{extension}である必要があります",
|
||||
"file_extension_must_not_be": "ファイル拡張子は{extension}であってはなりません",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "Je feedback blijft hangen :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "Rangschik alle opties",
|
||||
"file_extension_must_be": "Bestandsextensie moet {extension} zijn",
|
||||
"file_extension_must_not_be": "Bestandsextensie mag niet {extension} zijn",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "Seu feedback está preso :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "Por favor, classifique todas as opções",
|
||||
"file_extension_must_be": "A extensão do arquivo deve ser {extension}",
|
||||
"file_extension_must_not_be": "A extensão do arquivo não deve ser {extension}",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "Feedback-ul tău este blocat :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "Vă rugăm să ordonați toate opțiunile",
|
||||
"file_extension_must_be": "Extensia fișierului trebuie să fie {extension}",
|
||||
"file_extension_must_not_be": "Extensia fișierului nu trebuie să fie {extension}",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "Ваш отзыв застрял :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "Пожалуйста, расставьте все варианты по порядку",
|
||||
"file_extension_must_be": "Расширение файла должно быть {extension}",
|
||||
"file_extension_must_not_be": "Расширение файла не должно быть {extension}",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "Din feedback fastnade :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "Vänligen rangordna alla alternativ",
|
||||
"file_extension_must_be": "Filändelsen måste vara {extension}",
|
||||
"file_extension_must_not_be": "Filändelsen får inte vara {extension}",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "Sizning fikr-mulohazangiz qotib qoldi :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "Iltimos, barcha variantlarni tartiblang",
|
||||
"file_extension_must_be": "Fayl kengaytmasi {extension} bo‘lishi kerak",
|
||||
"file_extension_must_not_be": "Fayl kengaytmasi {extension} bo‘lmasligi kerak",
|
||||
"file_input": {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"your_feedback_is_stuck": "您的反馈卡住了 :("
|
||||
},
|
||||
"errors": {
|
||||
"all_options_must_be_ranked": "请对所有选项进行排序",
|
||||
"file_extension_must_be": "文件扩展名必须为{extension}",
|
||||
"file_extension_must_not_be": "文件扩展名不能为{extension}",
|
||||
"file_input": {
|
||||
|
||||
@@ -149,10 +149,10 @@ export function BlockConditional({
|
||||
): boolean => {
|
||||
const isRequired = element.required;
|
||||
const isValueArray = Array.isArray(response);
|
||||
const allItemsRanked = isValueArray && response.length === element.choices.length;
|
||||
const atLeastOneRanked = isValueArray && response.length >= 1;
|
||||
|
||||
// If required: all items must be ranked
|
||||
if (isRequired && (!isValueArray || !allItemsRanked)) {
|
||||
// If required: at least 1 option must be ranked
|
||||
if (isRequired && (!isValueArray || !atLeastOneRanked)) {
|
||||
form.requestSubmit();
|
||||
return false;
|
||||
}
|
||||
|
||||
725
packages/surveys/src/lib/validation/evaluator.test.ts
Normal file
725
packages/surveys/src/lib/validation/evaluator.test.ts
Normal file
@@ -0,0 +1,725 @@
|
||||
import type { TFunction } from "i18next";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import type { TResponseData } from "@formbricks/types/responses";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import type {
|
||||
TSurveyAddressElement,
|
||||
TSurveyContactInfoElement,
|
||||
TSurveyElement,
|
||||
TSurveyMatrixElement,
|
||||
TSurveyOpenTextElement,
|
||||
TSurveyRankingElement,
|
||||
} from "@formbricks/types/surveys/elements";
|
||||
import { getFirstErrorMessage, validateBlockResponses, validateElementResponse } from "./evaluator";
|
||||
|
||||
// Mock translation function
|
||||
const mockT = vi.fn((key: string) => {
|
||||
return key;
|
||||
}) as unknown as TFunction;
|
||||
|
||||
// Mock getLocalizedValue
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
getLocalizedValue: (localizedString: Record<string, string> | undefined, languageCode: string): string => {
|
||||
if (!localizedString) return "";
|
||||
return localizedString[languageCode] || localizedString.default || "";
|
||||
},
|
||||
}));
|
||||
|
||||
describe("validateElementResponse", () => {
|
||||
describe("required field validation", () => {
|
||||
test("should return error when required field is empty", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: true,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "", "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0].ruleId).toBe("required");
|
||||
});
|
||||
|
||||
test("should return valid when required field has value", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: true,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "test value", "en", mockT);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("should return valid when field is not required", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "", "en", mockT);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle required ranking element - at least one ranked", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "rank1",
|
||||
type: TSurveyElementTypeEnum.Ranking,
|
||||
headline: { default: "Rank these" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "opt1", label: { default: "Option 1" } },
|
||||
{ id: "opt2", label: { default: "Option 2" } },
|
||||
],
|
||||
} as unknown as TSurveyRankingElement;
|
||||
|
||||
const result = validateElementResponse(element, ["opt1"], "en", mockT);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return error when required ranking element has no ranked options", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "rank1",
|
||||
type: TSurveyElementTypeEnum.Ranking,
|
||||
headline: { default: "Rank these" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "opt1", label: { default: "Option 1" } },
|
||||
{ id: "opt2", label: { default: "Option 2" } },
|
||||
],
|
||||
} as unknown as TSurveyRankingElement;
|
||||
|
||||
const result = validateElementResponse(element, [], "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("should handle required matrix element - all rows must be answered", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "matrix1",
|
||||
type: TSurveyElementTypeEnum.Matrix,
|
||||
headline: { default: "Matrix question" },
|
||||
required: true,
|
||||
shuffleOption: "none",
|
||||
rows: [
|
||||
{ id: "row1", label: { default: "Row 1" } },
|
||||
{ id: "row2", label: { default: "Row 2" } },
|
||||
],
|
||||
columns: [
|
||||
{ id: "col1", label: { default: "Col 1" } },
|
||||
{ id: "col2", label: { default: "Col 2" } },
|
||||
],
|
||||
} as unknown as TSurveyMatrixElement;
|
||||
|
||||
const result = validateElementResponse(element, { row1: "col1", row2: "col2" }, "en", mockT);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return error when required matrix element has incomplete rows", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "matrix1",
|
||||
type: TSurveyElementTypeEnum.Matrix,
|
||||
headline: { default: "Matrix question" },
|
||||
required: true,
|
||||
shuffleOption: "none",
|
||||
rows: [
|
||||
{ id: "row1", label: { default: "Row 1" } },
|
||||
{ id: "row2", label: { default: "Row 2" } },
|
||||
],
|
||||
columns: [
|
||||
{ id: "col1", label: { default: "Col 1" } },
|
||||
{ id: "col2", label: { default: "Col 2" } },
|
||||
],
|
||||
} as unknown as TSurveyMatrixElement;
|
||||
|
||||
const result = validateElementResponse(element, { row1: "col1" }, "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validation rules - AND logic", () => {
|
||||
test("should return valid when all rules pass", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
validation: {
|
||||
rules: [
|
||||
{ id: "rule1", type: "minLength", params: { min: 5 } },
|
||||
{ id: "rule2", type: "maxLength", params: { max: 10 } },
|
||||
],
|
||||
logic: "and",
|
||||
},
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "hello", "en", mockT);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return error when one rule fails", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
validation: {
|
||||
rules: [
|
||||
{ id: "rule1", type: "minLength", params: { min: 10 } },
|
||||
{ id: "rule2", type: "maxLength", params: { max: 20 } },
|
||||
],
|
||||
logic: "and",
|
||||
},
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "hi", "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("should return multiple errors when multiple rules fail", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
validation: {
|
||||
rules: [
|
||||
{ id: "rule1", type: "minLength", params: { min: 10 } },
|
||||
{ id: "rule2", type: "maxLength", params: { max: 5 } },
|
||||
],
|
||||
logic: "and",
|
||||
},
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "hello", "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("should default to AND logic when logic is not specified", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
validation: {
|
||||
rules: [
|
||||
{ id: "rule1", type: "minLength", params: { min: 10 } },
|
||||
{ id: "rule2", type: "maxLength", params: { max: 5 } },
|
||||
],
|
||||
},
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "hello", "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validation rules - OR logic", () => {
|
||||
test("should return valid when at least one rule passes", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
validation: {
|
||||
rules: [
|
||||
{ id: "rule1", type: "minLength", params: { min: 10 } },
|
||||
{ id: "rule2", type: "maxLength", params: { max: 20 } },
|
||||
],
|
||||
logic: "or",
|
||||
},
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "hello", "en", mockT);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return error when all rules fail", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
validation: {
|
||||
rules: [
|
||||
{ id: "rule1", type: "minLength", params: { min: 10 } },
|
||||
{ id: "rule2", type: "maxLength", params: { max: 3 } },
|
||||
],
|
||||
logic: "or",
|
||||
},
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "hello", "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("implicit validation for OpenText inputType", () => {
|
||||
test("should add implicit email validation for email inputType", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
inputType: "email",
|
||||
required: false,
|
||||
charLimit: 0,
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "invalid-email", "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.ruleId === "__implicit_email__")).toBe(true);
|
||||
});
|
||||
|
||||
test("should add implicit url validation for url inputType", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
inputType: "url",
|
||||
required: false,
|
||||
charLimit: 0,
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "not-a-url", "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.ruleId === "__implicit_url__")).toBe(true);
|
||||
});
|
||||
|
||||
test("should add implicit phone validation for phone inputType", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
inputType: "phone",
|
||||
required: false,
|
||||
charLimit: 0,
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "invalid-phone", "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.ruleId === "__implicit_phone__")).toBe(true);
|
||||
});
|
||||
|
||||
test("should not add implicit rule if explicit rule exists", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
inputType: "email",
|
||||
required: false,
|
||||
charLimit: 0,
|
||||
validation: {
|
||||
rules: [{ id: "rule1", type: "email", params: {} }],
|
||||
},
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "test@example.com", "en", mockT);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors.some((e) => e.ruleId === "__implicit_email__")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("implicit validation for ContactInfo", () => {
|
||||
test("should add implicit email validation for email field", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "contact1",
|
||||
type: TSurveyElementTypeEnum.ContactInfo,
|
||||
headline: { default: "Contact Info" },
|
||||
firstName: { show: true, required: false, placeholder: { default: "First Name" } },
|
||||
lastName: { show: true, required: false, placeholder: { default: "Last Name" } },
|
||||
email: { show: true, required: false, placeholder: { default: "Email" } },
|
||||
phone: { show: false, required: false, placeholder: { default: "Phone" } },
|
||||
company: { show: false, required: false, placeholder: { default: "Company" } },
|
||||
required: false,
|
||||
} as unknown as TSurveyContactInfoElement;
|
||||
|
||||
const result = validateElementResponse(element, ["John", "Doe", "invalid-email", "", ""], "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.ruleId === "__implicit_email_field__")).toBe(true);
|
||||
});
|
||||
|
||||
test("should add implicit phone validation for phone field", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "contact1",
|
||||
type: TSurveyElementTypeEnum.ContactInfo,
|
||||
headline: { default: "Contact Info" },
|
||||
firstName: { show: true, required: false, placeholder: { default: "First Name" } },
|
||||
lastName: { show: true, required: false, placeholder: { default: "Last Name" } },
|
||||
email: { show: false, required: false, placeholder: { default: "Email" } },
|
||||
phone: { show: true, required: false, placeholder: { default: "Phone" } },
|
||||
company: { show: false, required: false, placeholder: { default: "Company" } },
|
||||
required: false,
|
||||
} as unknown as TSurveyContactInfoElement;
|
||||
|
||||
const result = validateElementResponse(element, ["John", "Doe", "", "invalid-phone", ""], "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.ruleId === "__implicit_phone_field__")).toBe(true);
|
||||
});
|
||||
|
||||
test("should not add implicit rule if explicit rule exists", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "contact1",
|
||||
type: TSurveyElementTypeEnum.ContactInfo,
|
||||
headline: { default: "Contact Info" },
|
||||
firstName: { show: true, required: false, placeholder: { default: "First Name" } },
|
||||
lastName: { show: true, required: false, placeholder: { default: "Last Name" } },
|
||||
email: { show: true, required: false, placeholder: { default: "Email" } },
|
||||
phone: { show: false, required: false, placeholder: { default: "Phone" } },
|
||||
company: { show: false, required: false, placeholder: { default: "Company" } },
|
||||
required: false,
|
||||
validation: {
|
||||
rules: [{ id: "rule1", type: "email", field: "email", params: {} }],
|
||||
},
|
||||
} as unknown as TSurveyContactInfoElement;
|
||||
|
||||
const result = validateElementResponse(
|
||||
element,
|
||||
["John", "Doe", "test@example.com", "", ""],
|
||||
"en",
|
||||
mockT
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors.some((e) => e.ruleId === "__implicit_email_field__")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("field-specific validation for Address", () => {
|
||||
test("should validate specific field in address element", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "address1",
|
||||
type: TSurveyElementTypeEnum.Address,
|
||||
headline: { default: "Address" },
|
||||
addressLine1: { show: true, required: false, placeholder: { default: "Address Line 1" } },
|
||||
addressLine2: { show: false, required: false, placeholder: { default: "Address Line 2" } },
|
||||
city: { show: true, required: false, placeholder: { default: "City" } },
|
||||
state: { show: true, required: false, placeholder: { default: "State" } },
|
||||
zip: { show: true, required: false, placeholder: { default: "ZIP" } },
|
||||
country: { show: true, required: false, placeholder: { default: "Country" } },
|
||||
required: false,
|
||||
validation: {
|
||||
rules: [{ id: "rule1", type: "minLength", field: "city", params: { min: 3 } }],
|
||||
},
|
||||
} as unknown as TSurveyAddressElement;
|
||||
|
||||
const result = validateElementResponse(element, ["123 Main St", "", "NY", "", "", ""], "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should validate correct field value", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "address1",
|
||||
type: TSurveyElementTypeEnum.Address,
|
||||
headline: { default: "Address" },
|
||||
addressLine1: { show: true, required: false, placeholder: { default: "Address Line 1" } },
|
||||
addressLine2: { show: false, required: false, placeholder: { default: "Address Line 2" } },
|
||||
city: { show: true, required: false, placeholder: { default: "City" } },
|
||||
state: { show: true, required: false, placeholder: { default: "State" } },
|
||||
zip: { show: true, required: false, placeholder: { default: "ZIP" } },
|
||||
country: { show: true, required: false, placeholder: { default: "Country" } },
|
||||
required: false,
|
||||
validation: {
|
||||
rules: [{ id: "rule1", type: "minLength", field: "city", params: { min: 3 } }],
|
||||
},
|
||||
} as unknown as TSurveyAddressElement;
|
||||
|
||||
const result = validateElementResponse(
|
||||
element,
|
||||
["123 Main St", "", "New York", "", "", ""],
|
||||
"en",
|
||||
mockT
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("field-specific validation for ContactInfo", () => {
|
||||
test("should validate specific field in contact info element", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "contact1",
|
||||
type: TSurveyElementTypeEnum.ContactInfo,
|
||||
headline: { default: "Contact Info" },
|
||||
firstName: { show: true, required: false, placeholder: { default: "First Name" } },
|
||||
lastName: { show: true, required: false, placeholder: { default: "Last Name" } },
|
||||
email: { show: true, required: false, placeholder: { default: "Email" } },
|
||||
phone: { show: true, required: false, placeholder: { default: "Phone" } },
|
||||
company: { show: false, required: false, placeholder: { default: "Company" } },
|
||||
required: false,
|
||||
validation: {
|
||||
rules: [{ id: "rule1", type: "minLength", field: "firstName", params: { min: 3 } }],
|
||||
},
|
||||
} as unknown as TSurveyContactInfoElement;
|
||||
|
||||
const result = validateElementResponse(
|
||||
element,
|
||||
["Jo", "Doe", "test@example.com", "1234567890", ""],
|
||||
"en",
|
||||
mockT
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matrix element validation rules", () => {
|
||||
test("should skip validation rules when matrix is required", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "matrix1",
|
||||
type: TSurveyElementTypeEnum.Matrix,
|
||||
headline: { default: "Matrix question" },
|
||||
required: true,
|
||||
shuffleOption: "none",
|
||||
rows: [
|
||||
{ id: "row1", label: { default: "Row 1" } },
|
||||
{ id: "row2", label: { default: "Row 2" } },
|
||||
],
|
||||
columns: [
|
||||
{ id: "col1", label: { default: "Col 1" } },
|
||||
{ id: "col2", label: { default: "Col 2" } },
|
||||
],
|
||||
validation: {
|
||||
rules: [{ id: "rule1", type: "minRowsAnswered", params: { min: 1 } }],
|
||||
},
|
||||
} as unknown as TSurveyMatrixElement;
|
||||
|
||||
const result = validateElementResponse(element, { row1: "col1", row2: "col2" }, "en", mockT);
|
||||
// Should only check required, not validation rules
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should apply validation rules when matrix is not required", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "matrix1",
|
||||
type: TSurveyElementTypeEnum.Matrix,
|
||||
headline: { default: "Matrix question" },
|
||||
required: false,
|
||||
shuffleOption: "none",
|
||||
rows: [
|
||||
{ id: "row1", label: { default: "Row 1" } },
|
||||
{ id: "row2", label: { default: "Row 2" } },
|
||||
{ id: "row3", label: { default: "Row 3" } },
|
||||
],
|
||||
columns: [
|
||||
{ id: "col1", label: { default: "Col 1" } },
|
||||
{ id: "col2", label: { default: "Col 2" } },
|
||||
],
|
||||
validation: {
|
||||
rules: [{ id: "rule1", type: "minRowsAnswered", params: { min: 2 } }],
|
||||
},
|
||||
} as unknown as TSurveyMatrixElement;
|
||||
|
||||
const result = validateElementResponse(element, { row1: "col1" }, "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("custom error messages", () => {
|
||||
test("should use custom error message when provided", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
validation: {
|
||||
rules: [
|
||||
{
|
||||
id: "rule1",
|
||||
type: "minLength",
|
||||
params: { min: 10 },
|
||||
customErrorMessage: { default: "Custom error message" },
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "short", "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0].message).toBe("Custom error message");
|
||||
});
|
||||
|
||||
test("should use language-specific custom error message", () => {
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
validation: {
|
||||
rules: [
|
||||
{
|
||||
id: "rule1",
|
||||
type: "minLength",
|
||||
params: { min: 10 },
|
||||
customErrorMessage: { default: "Default message", en: "English message" },
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "short", "en", mockT);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0].message).toBe("English message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unknown validation rule type", () => {
|
||||
test("should handle unknown rule type gracefully", () => {
|
||||
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const element: TSurveyElement = {
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
validation: {
|
||||
rules: [{ id: "rule1", type: "unknown" as any, params: {} }],
|
||||
},
|
||||
} as unknown as TSurveyOpenTextElement;
|
||||
|
||||
const result = validateElementResponse(element, "test", "en", mockT);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateBlockResponses", () => {
|
||||
test("should return empty error map when all elements are valid", () => {
|
||||
const elements: TSurveyElement[] = [
|
||||
{
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
} as TSurveyOpenTextElement,
|
||||
{
|
||||
id: "text2",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question 2" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
} as TSurveyOpenTextElement,
|
||||
];
|
||||
|
||||
const responses: TResponseData = {
|
||||
text1: "value1",
|
||||
text2: "value2",
|
||||
};
|
||||
|
||||
const result = validateBlockResponses(elements, responses, "en", mockT);
|
||||
expect(Object.keys(result)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("should return error map with invalid elements", () => {
|
||||
const elements: TSurveyElement[] = [
|
||||
{
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
} as TSurveyOpenTextElement,
|
||||
{
|
||||
id: "text2",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question 2" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
} as TSurveyOpenTextElement,
|
||||
];
|
||||
|
||||
const responses: TResponseData = {
|
||||
text1: "",
|
||||
text2: "value2",
|
||||
};
|
||||
|
||||
const result = validateBlockResponses(elements, responses, "en", mockT);
|
||||
expect(Object.keys(result)).toHaveLength(1);
|
||||
const text1Errors = result.text1;
|
||||
expect(text1Errors).toBeDefined();
|
||||
expect(text1Errors?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("should handle missing responses", () => {
|
||||
const elements: TSurveyElement[] = [
|
||||
{
|
||||
id: "text1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: true,
|
||||
inputType: "text",
|
||||
charLimit: 0,
|
||||
} as TSurveyOpenTextElement,
|
||||
];
|
||||
|
||||
const responses: TResponseData = {};
|
||||
|
||||
const result = validateBlockResponses(elements, responses, "en", mockT);
|
||||
expect(Object.keys(result)).toHaveLength(1);
|
||||
expect(result.text1).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFirstErrorMessage", () => {
|
||||
test("should return first error message for element", () => {
|
||||
const errorMap = {
|
||||
text1: [
|
||||
{ ruleId: "rule1", ruleType: "minLength" as const, message: "First error" },
|
||||
{ ruleId: "rule2", ruleType: "maxLength" as const, message: "Second error" },
|
||||
],
|
||||
};
|
||||
|
||||
const message = getFirstErrorMessage(errorMap, "text1");
|
||||
expect(message).toBe("First error");
|
||||
});
|
||||
|
||||
test("should return undefined when element has no errors", () => {
|
||||
const errorMap = {
|
||||
text1: [{ ruleId: "rule1", ruleType: "minLength" as const, message: "Error" }],
|
||||
};
|
||||
|
||||
const message = getFirstErrorMessage(errorMap, "text2");
|
||||
expect(message).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return undefined when error map is empty", () => {
|
||||
const errorMap = {};
|
||||
const message = getFirstErrorMessage(errorMap, "text1");
|
||||
expect(message).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -118,11 +118,11 @@ export const validateElementResponse = (
|
||||
// Special handling for ranking elements
|
||||
if (element.type === TSurveyElementTypeEnum.Ranking) {
|
||||
const isValueArray = Array.isArray(value);
|
||||
const allItemsRanked = isValueArray && value.length === element.choices.length;
|
||||
const atLeastOneRanked = isValueArray && value.length >= 1;
|
||||
|
||||
// If required: all items must be ranked (no partial ranking allowed)
|
||||
// If required: at least 1 option must be ranked
|
||||
// If not required: partial ranking is allowed (validation only checks if empty)
|
||||
if (isEmpty(value) || (isValueArray && !allItemsRanked)) {
|
||||
if (isEmpty(value) || !atLeastOneRanked) {
|
||||
errors.push(createRequiredError(t));
|
||||
}
|
||||
}
|
||||
|
||||
889
packages/surveys/src/lib/validation/validators.test.ts
Normal file
889
packages/surveys/src/lib/validation/validators.test.ts
Normal file
@@ -0,0 +1,889 @@
|
||||
import type { TFunction } from "i18next";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import type { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import { validators } from "./validators";
|
||||
|
||||
// Mock translation function - just return the key for testing
|
||||
const mockT = vi.fn((key: string) => {
|
||||
return key;
|
||||
}) as unknown as TFunction;
|
||||
|
||||
describe("validators", () => {
|
||||
describe("minLength", () => {
|
||||
test("should return valid true when string length >= min", () => {
|
||||
const result = validators.minLength.check("hello", { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when string length < min", () => {
|
||||
const result = validators.minLength.check("hi", { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty string", () => {
|
||||
const result = validators.minLength.check("", { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid true when value is not a string", () => {
|
||||
const result = validators.minLength.check(123, { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.minLength.getDefaultMessage({ min: 10 }, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.min_length");
|
||||
});
|
||||
});
|
||||
|
||||
describe("maxLength", () => {
|
||||
test("should return valid true when string length <= max", () => {
|
||||
const result = validators.maxLength.check("hello", { max: 10 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when string length > max", () => {
|
||||
const result = validators.maxLength.check("hello world", { max: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is not a string", () => {
|
||||
const result = validators.maxLength.check(123, { max: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.maxLength.getDefaultMessage({ max: 100 }, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.max_length");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pattern", () => {
|
||||
test("should return valid true when pattern matches", () => {
|
||||
const result = validators.pattern.check("Hello", { pattern: "^[A-Z]" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when pattern does not match", () => {
|
||||
const result = validators.pattern.check("hello", { pattern: "^[A-Z]" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.pattern.check("", { pattern: "^[A-Z]" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle regex flags", () => {
|
||||
const result = validators.pattern.check(
|
||||
"hello",
|
||||
{ pattern: "^[A-Z]", flags: "i" },
|
||||
{} as TSurveyElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should reject patterns longer than 512 chars", () => {
|
||||
const longPattern = "a".repeat(513);
|
||||
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const result = validators.pattern.check("test", { pattern: longPattern }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("should reject values longer than 4096 chars", () => {
|
||||
const longValue = "a".repeat(4097);
|
||||
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const result = validators.pattern.check(longValue, { pattern: ".*" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("should handle invalid regex gracefully", () => {
|
||||
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const result = validators.pattern.check("test", { pattern: "[invalid" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true); // Returns valid for invalid regex
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.pattern.getDefaultMessage({ pattern: ".*" }, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.invalid_format");
|
||||
});
|
||||
});
|
||||
|
||||
describe("email", () => {
|
||||
test("should return valid true for valid email", () => {
|
||||
const result = validators.email.check("test@example.com", {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false for invalid email", () => {
|
||||
const result = validators.email.check("invalid-email", {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.email.check("", {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid true when value is not a string", () => {
|
||||
const result = validators.email.check(123, {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.email.getDefaultMessage({}, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.please_enter_a_valid_email_address");
|
||||
});
|
||||
});
|
||||
|
||||
describe("url", () => {
|
||||
test("should return valid true for valid URL", () => {
|
||||
const result = validators.url.check("https://example.com", {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false for invalid URL", () => {
|
||||
const result = validators.url.check("not-a-url", {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.url.check("", {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.url.getDefaultMessage({}, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.please_enter_a_valid_url");
|
||||
});
|
||||
});
|
||||
|
||||
describe("phone", () => {
|
||||
test("should return valid true for valid phone number", () => {
|
||||
const result = validators.phone.check("+1234567890", {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid true for phone with spaces and dashes", () => {
|
||||
const result = validators.phone.check("+1 234-567-890", {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false for invalid phone", () => {
|
||||
const result = validators.phone.check("abc123", {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.phone.check("", {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.phone.getDefaultMessage({}, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.please_enter_a_valid_phone_number");
|
||||
});
|
||||
});
|
||||
|
||||
describe("minValue", () => {
|
||||
test("should return valid true when value >= min", () => {
|
||||
const result = validators.minValue.check(10, { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when value < min", () => {
|
||||
const result = validators.minValue.check(3, { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle string numbers", () => {
|
||||
const result = validators.minValue.check("10", { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.minValue.check("", { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid true for non-numeric values", () => {
|
||||
const result = validators.minValue.check("abc", { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.minValue.getDefaultMessage({ min: 10 }, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.min_value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("maxValue", () => {
|
||||
test("should return valid true when value <= max", () => {
|
||||
const result = validators.maxValue.check(5, { max: 10 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when value > max", () => {
|
||||
const result = validators.maxValue.check(15, { max: 10 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle string numbers", () => {
|
||||
const result = validators.maxValue.check("5", { max: 10 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.maxValue.check("", { max: 10 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.maxValue.getDefaultMessage({ max: 100 }, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.max_value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("minSelections", () => {
|
||||
test("should return valid true when selection count >= min", () => {
|
||||
const result = validators.minSelections.check(["opt1", "opt2"], { min: 2 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when selection count < min", () => {
|
||||
const result = validators.minSelections.check(["opt1"], { min: 2 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid false when value is not an array", () => {
|
||||
const result = validators.minSelections.check("not-array", { min: 2 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle 'other' option correctly", () => {
|
||||
const result = validators.minSelections.check(["opt1", "", "custom"], { min: 2 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.minSelections.getDefaultMessage({ min: 2 }, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.min_selections");
|
||||
});
|
||||
});
|
||||
|
||||
describe("maxSelections", () => {
|
||||
test("should return valid true when selection count <= max", () => {
|
||||
const result = validators.maxSelections.check(["opt1", "opt2"], { max: 3 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when selection count > max", () => {
|
||||
const result = validators.maxSelections.check(
|
||||
["opt1", "opt2", "opt3", "opt4"],
|
||||
{ max: 3 },
|
||||
{} as TSurveyElement
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is not an array", () => {
|
||||
const result = validators.maxSelections.check("not-array", { max: 3 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.maxSelections.getDefaultMessage({ max: 5 }, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.max_selections");
|
||||
});
|
||||
});
|
||||
|
||||
describe("equals", () => {
|
||||
test("should return valid true when value equals", () => {
|
||||
const result = validators.equals.check("test", { value: "test" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when value does not equal", () => {
|
||||
const result = validators.equals.check("test", { value: "other" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.equals.check("", { value: "test" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.equals.getDefaultMessage({ value: "test" }, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.value_must_equal");
|
||||
});
|
||||
});
|
||||
|
||||
describe("doesNotEqual", () => {
|
||||
test("should return valid true when value does not equal", () => {
|
||||
const result = validators.doesNotEqual.check("test", { value: "other" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when value equals", () => {
|
||||
const result = validators.doesNotEqual.check("test", { value: "test" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.doesNotEqual.check("", { value: "test" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.doesNotEqual.getDefaultMessage(
|
||||
{ value: "test" },
|
||||
{} as TSurveyElement,
|
||||
mockT
|
||||
);
|
||||
expect(message).toBe("errors.value_must_not_equal");
|
||||
});
|
||||
});
|
||||
|
||||
describe("contains", () => {
|
||||
test("should return valid true when value contains substring", () => {
|
||||
const result = validators.contains.check("hello world", { value: "world" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when value does not contain substring", () => {
|
||||
const result = validators.contains.check("hello", { value: "world" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.contains.check("", { value: "test" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.contains.getDefaultMessage({ value: "test" }, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.value_must_contain");
|
||||
});
|
||||
});
|
||||
|
||||
describe("doesNotContain", () => {
|
||||
test("should return valid true when value does not contain substring", () => {
|
||||
const result = validators.doesNotContain.check("hello", { value: "world" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when value contains substring", () => {
|
||||
const result = validators.doesNotContain.check("hello world", { value: "world" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.doesNotContain.check("", { value: "test" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.doesNotContain.getDefaultMessage(
|
||||
{ value: "test" },
|
||||
{} as TSurveyElement,
|
||||
mockT
|
||||
);
|
||||
expect(message).toBe("errors.value_must_not_contain");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isGreaterThan", () => {
|
||||
test("should return valid true when value > min", () => {
|
||||
const result = validators.isGreaterThan.check(10, { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when value <= min", () => {
|
||||
const result = validators.isGreaterThan.check(5, { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.isGreaterThan.check("", { min: 5 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.isGreaterThan.getDefaultMessage({ min: 10 }, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.is_greater_than");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isLessThan", () => {
|
||||
test("should return valid true when value < max", () => {
|
||||
const result = validators.isLessThan.check(5, { max: 10 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when value >= max", () => {
|
||||
const result = validators.isLessThan.check(10, { max: 10 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.isLessThan.check("", { max: 10 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.isLessThan.getDefaultMessage({ max: 100 }, {} as TSurveyElement, mockT);
|
||||
expect(message).toBe("errors.is_less_than");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isLaterThan", () => {
|
||||
test("should return valid true when date is later", () => {
|
||||
const result = validators.isLaterThan.check("2024-12-31", { date: "2024-01-01" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when date is not later", () => {
|
||||
const result = validators.isLaterThan.check("2024-01-01", { date: "2024-12-31" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.isLaterThan.check("", { date: "2024-01-01" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.isLaterThan.getDefaultMessage(
|
||||
{ date: "2024-01-01" },
|
||||
{} as TSurveyElement,
|
||||
mockT
|
||||
);
|
||||
expect(message).toBe("errors.is_later_than");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isEarlierThan", () => {
|
||||
test("should return valid true when date is earlier", () => {
|
||||
const result = validators.isEarlierThan.check(
|
||||
"2024-01-01",
|
||||
{ date: "2024-12-31" },
|
||||
{} as TSurveyElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when date is not earlier", () => {
|
||||
const result = validators.isEarlierThan.check(
|
||||
"2024-12-31",
|
||||
{ date: "2024-01-01" },
|
||||
{} as TSurveyElement
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.isEarlierThan.check("", { date: "2024-01-01" }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.isEarlierThan.getDefaultMessage(
|
||||
{ date: "2024-01-01" },
|
||||
{} as TSurveyElement,
|
||||
mockT
|
||||
);
|
||||
expect(message).toBe("errors.is_earlier_than");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isBetween", () => {
|
||||
test("should return valid true when date is between", () => {
|
||||
const result = validators.isBetween.check(
|
||||
"2024-06-15",
|
||||
{ startDate: "2024-01-01", endDate: "2024-12-31" },
|
||||
{} as TSurveyElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when date is not between", () => {
|
||||
const result = validators.isBetween.check(
|
||||
"2025-01-01",
|
||||
{ startDate: "2024-01-01", endDate: "2024-12-31" },
|
||||
{} as TSurveyElement
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.isBetween.check(
|
||||
"",
|
||||
{ startDate: "2024-01-01", endDate: "2024-12-31" },
|
||||
{} as TSurveyElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.isBetween.getDefaultMessage(
|
||||
{ startDate: "2024-01-01", endDate: "2024-12-31" },
|
||||
{} as TSurveyElement,
|
||||
mockT
|
||||
);
|
||||
expect(message).toBe("errors.is_between");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isNotBetween", () => {
|
||||
test("should return valid true when date is not between", () => {
|
||||
const result = validators.isNotBetween.check(
|
||||
"2025-01-01",
|
||||
{ startDate: "2024-01-01", endDate: "2024-12-31" },
|
||||
{} as TSurveyElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when date is between", () => {
|
||||
const result = validators.isNotBetween.check(
|
||||
"2024-06-15",
|
||||
{ startDate: "2024-01-01", endDate: "2024-12-31" },
|
||||
{} as TSurveyElement
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.isNotBetween.check(
|
||||
"",
|
||||
{ startDate: "2024-01-01", endDate: "2024-12-31" },
|
||||
{} as TSurveyElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.isNotBetween.getDefaultMessage(
|
||||
{ startDate: "2024-01-01", endDate: "2024-12-31" },
|
||||
{} as TSurveyElement,
|
||||
mockT
|
||||
);
|
||||
expect(message).toBe("errors.is_not_between");
|
||||
});
|
||||
});
|
||||
|
||||
describe("minRanked", () => {
|
||||
const rankingElement: TSurveyElement = {
|
||||
id: "rank1",
|
||||
type: TSurveyElementTypeEnum.Ranking,
|
||||
headline: { default: "Rank these" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "opt1", label: { default: "Option 1" } },
|
||||
{ id: "opt2", label: { default: "Option 2" } },
|
||||
{ id: "opt3", label: { default: "Option 3" } },
|
||||
],
|
||||
} as TSurveyElement;
|
||||
|
||||
test("should return valid true when ranked count >= min", () => {
|
||||
const result = validators.minRanked.check(["opt1", "opt2"], { min: 2 }, rankingElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when ranked count < min", () => {
|
||||
const result = validators.minRanked.check(["opt1"], { min: 2 }, rankingElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.minRanked.check([], { min: 2 }, rankingElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid true when element is not ranking", () => {
|
||||
const result = validators.minRanked.check(["opt1"], { min: 2 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.minRanked.getDefaultMessage({ min: 2 }, rankingElement, mockT);
|
||||
expect(message).toBe("errors.minimum_options_ranked");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rankAll", () => {
|
||||
const rankingElement: TSurveyElement = {
|
||||
id: "rank1",
|
||||
type: TSurveyElementTypeEnum.Ranking,
|
||||
headline: { default: "Rank these" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "opt1", label: { default: "Option 1" } },
|
||||
{ id: "opt2", label: { default: "Option 2" } },
|
||||
{ id: "opt3", label: { default: "Option 3" } },
|
||||
],
|
||||
} as TSurveyElement;
|
||||
|
||||
test("should return valid true when all options are ranked", () => {
|
||||
const result = validators.rankAll.check(["opt1", "opt2", "opt3"], {}, rankingElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when not all options are ranked", () => {
|
||||
const result = validators.rankAll.check(["opt1", "opt2"], {}, rankingElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.rankAll.check([], {}, rankingElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid true when element is not ranking", () => {
|
||||
const result = validators.rankAll.check(["opt1"], {}, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.rankAll.getDefaultMessage({}, rankingElement, mockT);
|
||||
expect(message).toBe("errors.all_options_must_be_ranked");
|
||||
});
|
||||
});
|
||||
|
||||
describe("minRowsAnswered", () => {
|
||||
const matrixElement: TSurveyElement = {
|
||||
id: "matrix1",
|
||||
type: TSurveyElementTypeEnum.Matrix,
|
||||
headline: { default: "Matrix question" },
|
||||
required: false,
|
||||
shuffleOption: "none",
|
||||
rows: [
|
||||
{ id: "row1", label: { default: "Row 1" } },
|
||||
{ id: "row2", label: { default: "Row 2" } },
|
||||
{ id: "row3", label: { default: "Row 3" } },
|
||||
],
|
||||
columns: [
|
||||
{ id: "col1", label: { default: "Col 1" } },
|
||||
{ id: "col2", label: { default: "Col 2" } },
|
||||
],
|
||||
} as TSurveyElement;
|
||||
|
||||
test("should return valid true when answered rows >= min", () => {
|
||||
const result = validators.minRowsAnswered.check(
|
||||
{ row1: "col1", row2: "col2" },
|
||||
{ min: 2 },
|
||||
matrixElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when answered rows < min", () => {
|
||||
const result = validators.minRowsAnswered.check({ row1: "col1" }, { min: 2 }, matrixElement);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
// Empty object has 0 answered rows, which is less than min (2), so it should fail
|
||||
// But if we pass undefined, it should skip validation
|
||||
const result = validators.minRowsAnswered.check(undefined, { min: 2 }, matrixElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid true when element is not matrix", () => {
|
||||
const result = validators.minRowsAnswered.check({ row1: "col1" }, { min: 2 }, {} as TSurveyElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should filter out empty values", () => {
|
||||
const result = validators.minRowsAnswered.check({ row1: "col1", row2: "" }, { min: 1 }, matrixElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.minRowsAnswered.getDefaultMessage({ min: 2 }, matrixElement, mockT);
|
||||
expect(message).toBe("errors.minimum_rows_answered");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fileExtensionIs", () => {
|
||||
const fileUploadElement: TSurveyElement = {
|
||||
id: "file1",
|
||||
type: TSurveyElementTypeEnum.FileUpload,
|
||||
headline: { default: "Upload file" },
|
||||
required: false,
|
||||
allowMultipleFiles: false,
|
||||
} as TSurveyElement;
|
||||
|
||||
test("should return valid true when file extension matches", () => {
|
||||
const result = validators.fileExtensionIs.check(
|
||||
["https://example.com/file.pdf"],
|
||||
{ extensions: ["pdf"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid true when file extension matches with dot", () => {
|
||||
const result = validators.fileExtensionIs.check(
|
||||
["https://example.com/file.pdf"],
|
||||
{ extensions: [".pdf"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when file extension does not match", () => {
|
||||
const result = validators.fileExtensionIs.check(
|
||||
["https://example.com/file.pdf"],
|
||||
{ extensions: ["jpg"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle multiple files", () => {
|
||||
const result = validators.fileExtensionIs.check(
|
||||
["https://example.com/file1.pdf", "https://example.com/file2.pdf"],
|
||||
{ extensions: ["pdf"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false if any file does not match", () => {
|
||||
const result = validators.fileExtensionIs.check(
|
||||
["https://example.com/file1.pdf", "https://example.com/file2.jpg"],
|
||||
{ extensions: ["pdf"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle URLs with query parameters", () => {
|
||||
const result = validators.fileExtensionIs.check(
|
||||
["https://example.com/file.pdf?token=123"],
|
||||
{ extensions: ["pdf"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false for files without extension", () => {
|
||||
const result = validators.fileExtensionIs.check(
|
||||
["https://example.com/file"],
|
||||
{ extensions: ["pdf"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.fileExtensionIs.check([], { extensions: ["pdf"] }, fileUploadElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid true when element is not fileUpload", () => {
|
||||
const result = validators.fileExtensionIs.check(
|
||||
["https://example.com/file.pdf"],
|
||||
{ extensions: ["pdf"] },
|
||||
{} as TSurveyElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle case-insensitive extensions", () => {
|
||||
const result = validators.fileExtensionIs.check(
|
||||
["https://example.com/file.PDF"],
|
||||
{ extensions: ["pdf"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.fileExtensionIs.getDefaultMessage(
|
||||
{ extensions: ["pdf", "jpg"] },
|
||||
fileUploadElement,
|
||||
mockT
|
||||
);
|
||||
expect(message).toBe("errors.file_extension_must_be");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fileExtensionIsNot", () => {
|
||||
const fileUploadElement: TSurveyElement = {
|
||||
id: "file1",
|
||||
type: TSurveyElementTypeEnum.FileUpload,
|
||||
headline: { default: "Upload file" },
|
||||
required: false,
|
||||
allowMultipleFiles: false,
|
||||
} as TSurveyElement;
|
||||
|
||||
test("should return valid true when file extension does not match", () => {
|
||||
const result = validators.fileExtensionIsNot.check(
|
||||
["https://example.com/file.pdf"],
|
||||
{ extensions: ["jpg"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false when file extension matches", () => {
|
||||
const result = validators.fileExtensionIsNot.check(
|
||||
["https://example.com/file.pdf"],
|
||||
{ extensions: ["pdf"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true for files without extension", () => {
|
||||
const result = validators.fileExtensionIsNot.check(
|
||||
["https://example.com/file"],
|
||||
{ extensions: ["pdf"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle multiple files", () => {
|
||||
const result = validators.fileExtensionIsNot.check(
|
||||
["https://example.com/file1.jpg", "https://example.com/file2.png"],
|
||||
{ extensions: ["pdf"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return valid false if any file matches forbidden extension", () => {
|
||||
const result = validators.fileExtensionIsNot.check(
|
||||
["https://example.com/file1.pdf", "https://example.com/file2.jpg"],
|
||||
{ extensions: ["pdf"] },
|
||||
fileUploadElement
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("should return valid true when value is empty", () => {
|
||||
const result = validators.fileExtensionIsNot.check([], { extensions: ["pdf"] }, fileUploadElement);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should return correct error message", () => {
|
||||
const message = validators.fileExtensionIsNot.getDefaultMessage(
|
||||
{ extensions: ["exe", "bat"] },
|
||||
fileUploadElement,
|
||||
mockT
|
||||
);
|
||||
expect(message).toBe("errors.file_extension_must_not_be");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -456,6 +456,27 @@ export const validators: Record<TValidationRuleType, TValidator> = {
|
||||
return t("errors.minimum_options_ranked", { min: typedParams.min });
|
||||
},
|
||||
},
|
||||
rankAll: {
|
||||
check: (
|
||||
value: TResponseDataValue,
|
||||
_params: TValidationRuleParams,
|
||||
element: TSurveyElement
|
||||
): TValidatorCheckResult => {
|
||||
if (element.type !== "ranking") {
|
||||
return { valid: true };
|
||||
}
|
||||
// Skip validation if value is empty
|
||||
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||
return { valid: true };
|
||||
}
|
||||
// All options must be ranked
|
||||
const allItemsRanked = value.length === element.choices.length;
|
||||
return { valid: allItemsRanked };
|
||||
},
|
||||
getDefaultMessage: (_params: TValidationRuleParams, _element: TSurveyElement, t: TFunction): string => {
|
||||
return t("errors.all_options_must_be_ranked");
|
||||
},
|
||||
},
|
||||
minRowsAnswered: {
|
||||
check: (
|
||||
value: TResponseDataValue,
|
||||
|
||||
@@ -32,12 +32,13 @@ export const ZValidationRuleType = z.enum([
|
||||
"isGreaterThan",
|
||||
"isLessThan",
|
||||
|
||||
// Selection rules (MultiSelect, PictureSelection)
|
||||
// Selection rules (MultiSelect)
|
||||
"minSelections",
|
||||
"maxSelections",
|
||||
|
||||
// Ranking rules
|
||||
"minRanked",
|
||||
"rankAll",
|
||||
|
||||
// Matrix rules
|
||||
"minRowsAnswered",
|
||||
@@ -139,6 +140,8 @@ export const ZValidationRuleParamsMinRanked = z.object({
|
||||
min: z.number().min(1),
|
||||
});
|
||||
|
||||
export const ZValidationRuleParamsRankAll = z.object({}).strict();
|
||||
|
||||
export const ZValidationRuleParamsMinRowsAnswered = z.object({
|
||||
min: z.number().min(1),
|
||||
});
|
||||
@@ -175,6 +178,7 @@ export const ZValidationRuleParams = z.union([
|
||||
ZValidationRuleParamsIsBetween,
|
||||
ZValidationRuleParamsIsNotBetween,
|
||||
ZValidationRuleParamsMinRanked,
|
||||
ZValidationRuleParamsRankAll,
|
||||
ZValidationRuleParamsMinRowsAnswered,
|
||||
ZValidationRuleParamsFileExtensionIs,
|
||||
ZValidationRuleParamsFileExtensionIsNot,
|
||||
@@ -204,6 +208,7 @@ export type TValidationRuleParamsIsEarlierThan = z.infer<typeof ZValidationRuleP
|
||||
export type TValidationRuleParamsIsBetween = z.infer<typeof ZValidationRuleParamsIsBetween>;
|
||||
export type TValidationRuleParamsIsNotBetween = z.infer<typeof ZValidationRuleParamsIsNotBetween>;
|
||||
export type TValidationRuleParamsMinRanked = z.infer<typeof ZValidationRuleParamsMinRanked>;
|
||||
export type TValidationRuleParamsRankAll = z.infer<typeof ZValidationRuleParamsRankAll>;
|
||||
export type TValidationRuleParamsMinRowsAnswered = z.infer<typeof ZValidationRuleParamsMinRowsAnswered>;
|
||||
export type TValidationRuleParamsFileExtensionIs = z.infer<typeof ZValidationRuleParamsFileExtensionIs>;
|
||||
export type TValidationRuleParamsFileExtensionIsNot = z.infer<typeof ZValidationRuleParamsFileExtensionIsNot>;
|
||||
@@ -245,12 +250,11 @@ const OPEN_TEXT_RULES = [
|
||||
const MULTIPLE_CHOICE_MULTI_RULES = ["minSelections", "maxSelections"] as const;
|
||||
const DATE_RULES = ["isLaterThan", "isEarlierThan", "isBetween", "isNotBetween"] as const;
|
||||
const MATRIX_RULES = ["minRowsAnswered"] as const;
|
||||
const RANKING_RULES = ["minRanked"] as const;
|
||||
const RANKING_RULES = ["minRanked", "rankAll"] as const;
|
||||
// Note: fileSizeAtLeast and fileSizeAtMost are not included because they cannot be validated
|
||||
// from response URLs alone (responses only contain file URLs, not file metadata).
|
||||
// File size validation happens client-side during upload via element.maxSizeInMB.
|
||||
const FILE_UPLOAD_RULES = ["fileExtensionIs", "fileExtensionIsNot"] as const;
|
||||
const PICTURE_SELECTION_RULES = ["minSelections", "maxSelections"] as const;
|
||||
// Address and Contact Info can use text-based validation rules on specific fields
|
||||
const ADDRESS_RULES = [
|
||||
"minLength",
|
||||
@@ -285,7 +289,7 @@ export const APPLICABLE_RULES: Record<string, TValidationRuleType[]> = {
|
||||
matrix: [...MATRIX_RULES],
|
||||
ranking: [...RANKING_RULES],
|
||||
fileUpload: [...FILE_UPLOAD_RULES],
|
||||
pictureSelection: [...PICTURE_SELECTION_RULES],
|
||||
pictureSelection: [],
|
||||
address: [...ADDRESS_RULES],
|
||||
contactInfo: [...CONTACT_INFO_RULES],
|
||||
};
|
||||
@@ -309,9 +313,6 @@ export type TValidationRulesForDate = TValidationRulesForElementType<typeof DATE
|
||||
export type TValidationRulesForMatrix = TValidationRulesForElementType<typeof MATRIX_RULES>;
|
||||
export type TValidationRulesForRanking = TValidationRulesForElementType<typeof RANKING_RULES>;
|
||||
export type TValidationRulesForFileUpload = TValidationRulesForElementType<typeof FILE_UPLOAD_RULES>;
|
||||
export type TValidationRulesForPictureSelection = TValidationRulesForElementType<
|
||||
typeof PICTURE_SELECTION_RULES
|
||||
>;
|
||||
export type TValidationRulesForAddress = TValidationRulesForElementType<typeof ADDRESS_RULES>;
|
||||
export type TValidationRulesForContactInfo = TValidationRulesForElementType<typeof CONTACT_INFO_RULES>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user