Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/types/BreadcrumbList.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default class BreadcrumbListValidator extends BaseValidator {
issueMessage: 'At least two ListItems are required',
severity: 'WARNING',
path: this.path,
fieldNames: ['itemListElement'],
};
}
return null;
Expand Down Expand Up @@ -137,6 +138,7 @@ export default class BreadcrumbListValidator extends BaseValidator {
issueMessage: e,
severity: 'WARNING',
path: newPath,
fieldNames: [urlPath || 'item'],
});
}
}
Expand Down
1 change: 1 addition & 0 deletions src/types/DefinedRegion.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default class DefinedRegionValidator extends BaseValidator {
issueMessage: 'Only one of addressRegion or postalCode can be used',
severity: 'WARNING',
path: this.path,
fieldNames: ['addressRegion', 'postalCode'],
};
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/types/MerchantReturnPolicy.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export default class MerchantReturnPolicyValidator extends BaseValidator {
'Either applicableCountry and returnPolicyCategory or merchantReturnLink must be present',
severity: 'ERROR',
path: this.path,
fieldNames: [
'applicableCountry',
'returnPolicyCategory',
'merchantReturnLink',
],
};
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/types/Product.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default class ProductValidator extends BaseValidator {
'At least 2 notes, either positive or negative, are required',
severity: 'WARNING',
path: this.path,
fieldNames: ['review.positiveNotes', 'review.negativeNotes'],
});
}

Expand All @@ -63,6 +64,7 @@ export default class ProductValidator extends BaseValidator {
'One of the following attributes is required: "aggregateRating", "offers" or "review"',
severity: 'ERROR',
path: this.path,
fieldNames: ['aggregateRating', 'offers', 'review'],
});
}

Expand Down
1 change: 1 addition & 0 deletions src/types/ProductMerchant.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export default class ProductMerchantValidator extends BaseValidator {
issueMessage: `Missing one of field ${gtinFields.map((a) => `"${a}"`).join(', ')} on either product or all offers`,
severity: 'WARNING',
path: this.path,
fieldNames: gtinFields,
};
}
}
1 change: 1 addition & 0 deletions src/types/Rating.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default class RatingValidator extends BaseValidator {
issueMessage: `Rating is outside the specified or default range`,
severity: 'ERROR',
path: this.path,
fieldNames: ['ratingValue'],
};
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/types/__tests__/BreadcrumbList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('BreadcrumbListValidator', () => {
issueMessage: 'At least two ListItems are required',
severity: 'WARNING',
path: [{ type: 'BreadcrumbList', index: 0 }],
fieldNames: ['itemListElement'],
});
});

Expand All @@ -77,6 +78,7 @@ describe('BreadcrumbListValidator', () => {
type: 'ListItem',
},
],
fieldNames: ['item'],
});
});

Expand Down Expand Up @@ -215,6 +217,7 @@ describe('BreadcrumbListValidator', () => {
type: 'ListItem',
},
],
fieldNames: ['item'],
});
});
});
Expand Down
1 change: 1 addition & 0 deletions src/types/__tests__/DefinedRegion.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('DefinedRegionValidator', () => {
index: 0,
},
],
fieldNames: ['addressRegion', 'postalCode'],
});
});
});
Expand Down
5 changes: 5 additions & 0 deletions src/types/__tests__/MerchantReturnPolicy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ describe('MerchantReturnPolicyValidator', () => {
index: 0,
},
],
fieldNames: [
'applicableCountry',
'returnPolicyCategory',
'merchantReturnLink',
],
});
});
});
Expand Down
2 changes: 2 additions & 0 deletions src/types/__tests__/Product.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ describe('ProductValidator', () => {
'One of the following attributes is required: "aggregateRating", "offers" or "review"',
location: '35,350',
severity: 'ERROR',
fieldNames: ['aggregateRating', 'offers', 'review'],
});
});

Expand All @@ -107,6 +108,7 @@ describe('ProductValidator', () => {
'At least 2 notes, either positive or negative, are required',
location: '35,1353',
severity: 'WARNING',
fieldNames: ['review.positiveNotes', 'review.negativeNotes'],
});
});

Expand Down
1 change: 1 addition & 0 deletions src/types/__tests__/ProductMerchant.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ describe('ProductMerchantListValidator', () => {
location: '35,1236',
severity: 'WARNING',
path: [{ type: 'Product', index: 0 }],
fieldNames: ['gtin', 'gtin8', 'gtin12', 'gtin13', 'gtin14', 'isbn'],
});
});
});
Expand Down
23 changes: 23 additions & 0 deletions src/types/__tests__/Rating.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,28 @@ describe('RatingValidator', () => {
const issues = await validator.validate(data);
expect(issues).to.deep.equal([]);
});

it('should return error with fieldNames when rating is outside range', async () => {
const data = {
jsonld: {
Rating: [
{
'@type': 'Rating',
'@location': '1,100',
ratingValue: 10,
bestRating: 5,
worstRating: 0,
},
],
},
};
const issues = await validator.validate(data);
expect(issues).to.have.lengthOf(1);
expect(issues[0]).to.deep.include({
issueMessage: 'Rating is outside the specified or default range',
severity: 'ERROR',
fieldNames: ['ratingValue'],
});
});
});
});
139 changes: 139 additions & 0 deletions src/types/__tests__/base.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { expect } from "chai";
import BaseValidator from "../base.js";

describe("BaseValidator", () => {
describe("fieldNames property", () => {
let validator;
const testPath = [{ type: "TestType" }];

beforeEach(() => {
validator = new BaseValidator({
dataFormat: "jsonld",
path: testPath,
});
});

describe("required()", () => {
it("should include fieldNames when required attribute is missing", () => {
const condition = validator.required("price");
const result = condition({});

expect(result).to.deep.include({
severity: "ERROR",
issueMessage: 'Required attribute "price" is missing',
});
expect(result.fieldNames).to.deep.equal(["price"]);
expect(result.path).to.deep.equal(testPath);
});

it("should include fieldNames when required attribute has invalid type", () => {
const condition = validator.required("price", "number");
const result = condition({ price: "not-a-number" });

expect(result).to.deep.include({
severity: "ERROR",
issueMessage: 'Invalid type for attribute "price"',
});
expect(result.fieldNames).to.deep.equal(["price"]);
});

it("should return null when required attribute is valid", () => {
const condition = validator.required("name");
const result = condition({ name: "Test Product" });

expect(result).to.be.null;
});
});

describe("recommended()", () => {
it("should include fieldNames when recommended attribute is missing", () => {
const condition = validator.recommended("description");
const result = condition({});

expect(result).to.deep.include({
severity: "WARNING",
issueMessage: 'Missing field "description" (optional)',
});
expect(result.fieldNames).to.deep.equal(["description"]);
expect(result.path).to.deep.equal(testPath);
});

it("should include fieldNames when recommended attribute has invalid type", () => {
const condition = validator.recommended("image", "url");
const result = condition({ image: "data:invalid" });

expect(result).to.deep.include({
severity: "WARNING",
issueMessage: 'Invalid type for attribute "image"',
});
expect(result.fieldNames).to.deep.equal(["image"]);
});

it("should return null when recommended attribute is valid", () => {
const condition = validator.recommended("description");
const result = condition({ description: "A great product" });

expect(result).to.be.null;
});
});

describe("or()", () => {
it("should include fieldNames when or conditions fail", () => {
const condition = validator.or(
validator.required("price"),
validator.required("priceSpecification.price")
);
const result = condition({});

expect(result.severity).to.equal("ERROR");
expect(result.fieldNames).to.deep.equal([
"price",
"priceSpecification.price",
]);
expect(result.path).to.deep.equal(testPath);
});

it("should return null when at least one or condition passes", () => {
const condition = validator.or(
validator.required("price"),
validator.required("priceSpecification.price")
);
const result = condition({ price: "19.99" });

expect(result).to.be.null;
});

it("should handle single failing condition in or()", () => {
const condition = validator.or(
validator.required("availability")
);
const result = condition({});

expect(result.fieldNames).to.deep.equal(["availability"]);
});
});

describe("nested path fieldNames", () => {
it("should include fieldNames for nested attributes", () => {
const condition = validator.required("offers.price");
const result = condition({ offers: {} });

expect(result).to.deep.include({
severity: "ERROR",
});
expect(result.fieldNames).to.deep.equal(["offers.price"]);
});
});
});
});
2 changes: 2 additions & 0 deletions src/types/__tests__/schemaOrg.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ describe('Schema.org Validator', () => {
severity: 'WARNING',
path: [{ type: 'Product', index: 0 }],
errorType: 'schemaOrg',
fieldNames: ['my-custom-attribute'],
});
});

Expand Down Expand Up @@ -154,6 +155,7 @@ describe('Schema.org Validator', () => {
},
],
errorType: 'schemaOrg',
fieldNames: ['my-custom-attribute'],
});
});
});
Expand Down
11 changes: 11 additions & 0 deletions src/types/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,15 @@ export default class BaseValidator {
issueMessage: `Required attribute "${name}" is missing`,
severity: 'ERROR',
path: this.path,
fieldNames: [name],
};
}
if (type && !this.checkType(value, type, ...opts)) {
return {
issueMessage: `Invalid type for attribute "${name}"`,
severity: 'ERROR',
path: this.path,
fieldNames: [name],
};
}
return null;
Expand All @@ -89,13 +91,20 @@ export default class BaseValidator {
return max;
}, 'WARNING');

// Collect all field names from the conditions
const fieldNames = issues
.flat()
.filter((i) => i && i.fieldNames)
.flatMap((i) => i.fieldNames);

return {
issueMessage: `One of the following conditions needs to be met: ${issues
.flat()
.map((c) => c.issueMessage)
.join(' or ')}`,
severity,
path: this.path,
fieldNames: fieldNames.length > 0 ? fieldNames : [],
};
};
}
Expand All @@ -108,13 +117,15 @@ export default class BaseValidator {
issueMessage: `Missing field "${name}" (optional)`,
severity: 'WARNING',
path: this.path,
fieldNames: [name],
};
}
if (type && !this.checkType(value, type, ...opts)) {
return {
issueMessage: `Invalid type for attribute "${name}"`,
severity: 'WARNING',
path: this.path,
fieldNames: [name],
};
}
return null;
Expand Down
1 change: 1 addition & 0 deletions src/types/schemaOrg.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export default class SchemaOrgValidator {
severity: 'WARNING',
path: this.path,
errorType: 'schemaOrg',
fieldNames: [propertyId],
});
}
}),
Expand Down