11// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22
33import { describe , it , expect } from 'vitest' ;
4- import { validateRecord , normalizeMultiValueFields } from './record-validator.js' ;
4+ import { validateRecord , normalizeMultiValueFields , ValidationError } from './record-validator.js' ;
55
66/**
77 * Required-field validation, with the autonumber exemption (#1603).
@@ -55,14 +55,14 @@ describe('validateRecord — time field accepts time-of-day', () => {
5555
5656 for ( const v of [ '25:00' , '14:60' , 'not-a-time' , '14' ] ) {
5757 it ( `rejects ${ v } ` , ( ) => {
58- expect ( ( ) => validateRecord ( schema , { at : v } , 'insert' ) ) . toThrow ( / i n v a l i d _ t i m e / i) ;
58+ expect ( ( ) => validateRecord ( schema , { at : v } , 'insert' ) ) . toThrow ( / m u s t b e a v a l i d t i m e / i) ;
5959 } ) ;
6060 }
6161
6262 it ( 'does NOT regress date/datetime (still ISO-parsed)' , ( ) => {
6363 const ds = { fields : { d : { type : 'date' } , dt : { type : 'datetime' } } } ;
6464 expect ( ( ) => validateRecord ( ds , { d : '2026-06-17' , dt : '2026-06-17T10:00:00Z' } , 'insert' ) ) . not . toThrow ( ) ;
65- expect ( ( ) => validateRecord ( ds , { d : 'not-a-date' } , 'insert' ) ) . toThrow ( / i n v a l i d _ d a t e / i) ;
65+ expect ( ( ) => validateRecord ( ds , { d : 'not-a-date' } , 'insert' ) ) . toThrow ( / m u s t b e a v a l i d d a t e / i) ;
6666 } ) ;
6767} ) ;
6868
@@ -160,13 +160,13 @@ describe('validateRecord — multi-value fields must be arrays', () => {
160160 { watchers : 'user-1' } ,
161161 { attachments : 'file-key-1' } ,
162162 ] ) {
163- expect ( ( ) => validateRecord ( schema , payload , 'update' ) ) . toThrow ( / i n v a l i d _ t y p e / i) ;
163+ expect ( ( ) => validateRecord ( schema , payload , 'update' ) ) . toThrow ( / m u s t b e a n a r r a y / i) ;
164164 }
165165 } ) ;
166166
167167 it ( 'rejects a plain-object shape with invalid_type' , ( ) => {
168- expect ( ( ) => validateRecord ( schema , { labels : { nested : true } } , 'update' ) ) . toThrow ( / i n v a l i d _ t y p e / i) ;
169- expect ( ( ) => validateRecord ( schema , { team_members : { id : 'u1' } } , 'update' ) ) . toThrow ( / i n v a l i d _ t y p e / i) ;
168+ expect ( ( ) => validateRecord ( schema , { labels : { nested : true } } , 'update' ) ) . toThrow ( / m u s t b e a n a r r a y / i) ;
169+ expect ( ( ) => validateRecord ( schema , { team_members : { id : 'u1' } } , 'update' ) ) . toThrow ( / m u s t b e a n a r r a y / i) ;
170170 } ) ;
171171
172172 it ( 'accepts arrays (including for select+multiple, previously mis-rejected)' , ( ) => {
@@ -180,12 +180,54 @@ describe('validateRecord — multi-value fields must be arrays', () => {
180180 } ) ;
181181
182182 it ( 'still validates array ELEMENTS against options' , ( ) => {
183- expect ( ( ) => validateRecord ( schema , { labels : [ 'nope' ] } , 'update' ) ) . toThrow ( / i n v a l i d _ o p t i o n / i) ;
184- expect ( ( ) => validateRecord ( schema , { channels : [ 'fax' ] } , 'update' ) ) . toThrow ( / i n v a l i d _ o p t i o n / i) ;
183+ expect ( ( ) => validateRecord ( schema , { labels : [ 'nope' ] } , 'update' ) ) . toThrow ( / i s n o t o n e o f / i) ;
184+ expect ( ( ) => validateRecord ( schema , { channels : [ 'fax' ] } , 'update' ) ) . toThrow ( / i s n o t o n e o f / i) ;
185185 } ) ;
186186
187187 it ( 'does NOT regress single select / radio' , ( ) => {
188188 expect ( ( ) => validateRecord ( schema , { status : 'active' } , 'update' ) ) . not . toThrow ( ) ;
189- expect ( ( ) => validateRecord ( schema , { status : 'nope' } , 'update' ) ) . toThrow ( / i n v a l i d _ o p t i o n / i) ;
189+ expect ( ( ) => validateRecord ( schema , { status : 'nope' } , 'update' ) ) . toThrow ( / m u s t b e o n e o f / i) ;
190+ } ) ;
191+ } ) ;
192+
193+ /**
194+ * The top-level `ValidationError.message` is what generic UI surfaces (the
195+ * console's save-error toast, CLI output) display verbatim — it must carry
196+ * the HUMAN per-field messages, not a `field (code)` digest. Regression for
197+ * the rule-violation case: an author-written localized rule `message`
198+ * ("最小水深不能大于最大水深。") used to be buried in `fields[]` while the
199+ * toast showed "Validation failed for 1 field(s): _record (rule_violation)".
200+ */
201+ describe ( 'ValidationError — top-level message is human-readable' , ( ) => {
202+ it ( 'uses each field error message verbatim' , ( ) => {
203+ const err = new ValidationError ( [
204+ { field : '_record' , code : 'rule_violation' , message : '最小水深不能大于最大水深。' } ,
205+ ] ) ;
206+ expect ( err . message ) . toBe ( '最小水深不能大于最大水深。' ) ;
207+ } ) ;
208+
209+ it ( 'joins multiple field messages' , ( ) => {
210+ const err = new ValidationError ( [
211+ { field : 'title' , code : 'required' , message : 'title is required' } ,
212+ { field : '_record' , code : 'rule_violation' , message : '最小水深不能大于最大水深。' } ,
213+ ] ) ;
214+ expect ( err . message ) . toBe ( 'title is required; 最小水深不能大于最大水深。' ) ;
215+ } ) ;
216+
217+ it ( 'falls back to `field (code)` when a message is blank' , ( ) => {
218+ const err = new ValidationError ( [
219+ { field : '_record' , code : 'rule_violation' , message : '' } ,
220+ ] ) ;
221+ expect ( err . message ) . toBe ( '_record (rule_violation)' ) ;
222+ } ) ;
223+
224+ it ( 'still exposes machine-readable fields[] for programmatic handling' , ( ) => {
225+ const err = new ValidationError ( [
226+ { field : '_record' , code : 'rule_violation' , message : 'boom' } ,
227+ ] ) ;
228+ expect ( err . code ) . toBe ( 'VALIDATION_FAILED' ) ;
229+ expect ( err . fields ) . toEqual ( [
230+ { field : '_record' , code : 'rule_violation' , message : 'boom' } ,
231+ ] ) ;
190232 } ) ;
191233} ) ;
0 commit comments