@@ -48,6 +48,29 @@ describe("presentation-4 validator", () => {
4848 expect ( report . issues . some ( ( issue ) => issue . code === "canvas-height-required" ) ) . toBe ( true ) ;
4949 } ) ;
5050
51+ test ( "prefers specific property requirement issues over generic class requirement issues" , ( ) => {
52+ const invalid = {
53+ "@context" : "http://iiif.io/api/presentation/4/context.json" ,
54+ id : "https://example.org/manifest/missing-canvas-height" ,
55+ type : "Manifest" ,
56+ label : { en : [ "missing canvas height" ] } ,
57+ items : [
58+ {
59+ id : "https://example.org/canvas/1" ,
60+ type : "Canvas" ,
61+ width : 1000 ,
62+ items : [ ] ,
63+ } ,
64+ ] ,
65+ } ;
66+
67+ const report = validatePresentation4 ( invalid ) ;
68+ const heightIssues = report . issues . filter ( ( issue ) => issue . path === "$.items[0].height" ) ;
69+
70+ expect ( heightIssues . some ( ( issue ) => issue . code === "canvas-height-required" ) ) . toBe ( true ) ;
71+ expect ( heightIssues . some ( ( issue ) => issue . code === "class-requirement-must" ) ) . toBe ( false ) ;
72+ } ) ;
73+
5174 test ( "rejects array-form annotation body/target in favor of object/List" , ( ) => {
5275 const invalid = {
5376 "@context" : "http://iiif.io/api/presentation/4/context.json" ,
@@ -795,4 +818,46 @@ describe("presentation-4 validator", () => {
795818 expect ( report . issues . some ( ( item ) => item . code === "class-requirement-property-not-listed" ) ) . toBe ( true ) ;
796819 expect ( report . issues . some ( ( item ) => item . code === "annotation-body-value-forbidden" ) ) . toBe ( true ) ;
797820 } ) ;
821+
822+ test ( "deduplicates exact duplicate issues emitted by multiple validation passes" , ( ) => {
823+ const invalid = {
824+ "@context" : "http://iiif.io/api/presentation/4/context.json" ,
825+ id : "https://example.org/manifest/annotation-body-value-dedupe" ,
826+ type : "Manifest" ,
827+ label : { en : [ "annotation body value dedupe" ] } ,
828+ items : [
829+ {
830+ id : "https://example.org/canvas/1" ,
831+ type : "Canvas" ,
832+ width : 1000 ,
833+ height : 1000 ,
834+ items : [
835+ {
836+ id : "https://example.org/canvas/1/page/1" ,
837+ type : "AnnotationPage" ,
838+ items : [
839+ {
840+ id : "https://example.org/canvas/1/page/1/anno/1" ,
841+ type : "Annotation" ,
842+ motivation : [ "commenting" ] ,
843+ target : {
844+ id : "https://example.org/canvas/1" ,
845+ type : "Canvas" ,
846+ } ,
847+ bodyValue : "invalid body value" ,
848+ } ,
849+ ] ,
850+ } ,
851+ ] ,
852+ } ,
853+ ] ,
854+ } ;
855+
856+ const report = validatePresentation4 ( invalid , { mode : "tolerant" } ) ;
857+ const bodyValueIssues = report . issues . filter ( ( issue ) => issue . code === "annotation-body-value-forbidden" ) ;
858+ const firstBodyValueIssue = bodyValueIssues . at ( 0 ) ;
859+
860+ expect ( bodyValueIssues ) . toHaveLength ( 1 ) ;
861+ expect ( firstBodyValueIssue ?. path ) . toBe ( "$.items[0].items[0].items[0].bodyValue" ) ;
862+ } ) ;
798863} ) ;
0 commit comments