/* * Copyright (C) 2023-2024 Yomitan Authors * Copyright (C) 2020-2022 Yomichan Authors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ import {describe, expect, test} from 'vitest'; import {createSchema, normalizeContext} from '../ext/js/background/profile-conditions-util.js'; describe('Profile conditions utilities', () => { describe('NormalizeContext', () => { /** @type {{context: import('settings').OptionsContext, expected: import('profile-conditions-util').NormalizedOptionsContext}[]} */ const data = [ // Empty { context: {index: 0}, expected: {index: 0, flags: []} }, // Domain normalization { context: {depth: 0, url: ''}, expected: {depth: 0, url: '', flags: []} }, { context: {depth: 0, url: 'http://example.com/'}, expected: {depth: 0, url: 'http://example.com/', domain: 'example.com', flags: []} }, { context: {depth: 0, url: 'http://example.com:1234/'}, expected: {depth: 0, url: 'http://example.com:1234/', domain: 'example.com', flags: []} }, { context: {depth: 0, url: 'http://user@example.com:1234/'}, expected: {depth: 0, url: 'http://user@example.com:1234/', domain: 'example.com', flags: []} } ]; test.each(data)('normalize-context-test-%#', ({context, expected}) => { const actual = normalizeContext(context); expect(actual).toStrictEqual(expected); }); }); describe('Schemas', () => { /* eslint-disable @stylistic/no-multi-spaces */ /** @type {{conditionGroups: import('settings').ProfileConditionGroup[], expectedSchema?: import('ext/json-schema').Schema, inputs?: {expected: boolean, context: import('settings').OptionsContext}[]}[]} */ const data = [ // Empty { conditionGroups: [], expectedSchema: {}, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/'}} ] }, { conditionGroups: [ {conditions: []} ], expectedSchema: {}, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/'}} ] }, { conditionGroups: [ {conditions: []}, {conditions: []} ], expectedSchema: {}, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/'}} ] }, // popupLevel tests { conditionGroups: [ { conditions: [ { type: 'popupLevel', operator: 'equal', value: '0' } ] } ], expectedSchema: { properties: { depth: {const: 0} }, required: ['depth'] }, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/'}}, {expected: false, context: {depth: 1, url: 'http://example.com/'}}, {expected: false, context: {depth: -1, url: 'http://example.com/'}} ] }, { conditionGroups: [ { conditions: [ { type: 'popupLevel', operator: 'notEqual', value: '0' } ] } ], expectedSchema: { not: { anyOf: [ { properties: { depth: {const: 0} }, required: ['depth'] } ] } }, inputs: [ {expected: false, context: {depth: 0, url: 'http://example.com/'}}, {expected: true, context: {depth: 1, url: 'http://example.com/'}}, {expected: true, context: {depth: -1, url: 'http://example.com/'}} ] }, { conditionGroups: [ { conditions: [ { type: 'popupLevel', operator: 'lessThan', value: '0' } ] } ], expectedSchema: { properties: { depth: { type: 'number', exclusiveMaximum: 0 } }, required: ['depth'] }, inputs: [ {expected: false, context: {depth: 0, url: 'http://example.com/'}}, {expected: false, context: {depth: 1, url: 'http://example.com/'}}, {expected: true, context: {depth: -1, url: 'http://example.com/'}} ] }, { conditionGroups: [ { conditions: [ { type: 'popupLevel', operator: 'greaterThan', value: '0' } ] } ], expectedSchema: { properties: { depth: { type: 'number', exclusiveMinimum: 0 } }, required: ['depth'] }, inputs: [ {expected: false, context: {depth: 0, url: 'http://example.com/'}}, {expected: true, context: {depth: 1, url: 'http://example.com/'}}, {expected: false, context: {depth: -1, url: 'http://example.com/'}} ] }, { conditionGroups: [ { conditions: [ { type: 'popupLevel', operator: 'lessThanOrEqual', value: '0' } ] } ], expectedSchema: { properties: { depth: { type: 'number', maximum: 0 } }, required: ['depth'] }, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/'}}, {expected: false, context: {depth: 1, url: 'http://example.com/'}}, {expected: true, context: {depth: -1, url: 'http://example.com/'}} ] }, { conditionGroups: [ { conditions: [ { type: 'popupLevel', operator: 'greaterThanOrEqual', value: '0' } ] } ], expectedSchema: { properties: { depth: { type: 'number', minimum: 0 } }, required: ['depth'] }, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/'}}, {expected: true, context: {depth: 1, url: 'http://example.com/'}}, {expected: false, context: {depth: -1, url: 'http://example.com/'}} ] }, // Url tests { conditionGroups: [ { conditions: [ { type: 'url', operator: 'matchDomain', value: 'example.com' } ] } ], expectedSchema: { properties: { domain: { oneOf: [ {const: 'example.com'} ] } }, required: ['domain'] }, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/'}}, {expected: false, context: {depth: 0, url: 'http://example1.com/'}}, {expected: false, context: {depth: 0, url: 'http://example2.com/'}}, {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}}, {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}} ] }, { conditionGroups: [ { conditions: [ { type: 'url', operator: 'matchDomain', value: 'example.com, example1.com, example2.com' } ] } ], expectedSchema: { properties: { domain: { oneOf: [ {const: 'example.com'}, {const: 'example1.com'}, {const: 'example2.com'} ] } }, required: ['domain'] }, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/'}}, {expected: true, context: {depth: 0, url: 'http://example1.com/'}}, {expected: true, context: {depth: 0, url: 'http://example2.com/'}}, {expected: false, context: {depth: 0, url: 'http://example3.com/'}}, {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}}, {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}} ] }, { conditionGroups: [ { conditions: [ { type: 'url', operator: 'matchRegExp', value: '^http://example\\d?\\.com/[\\w\\W]*$' } ] } ], expectedSchema: { properties: { url: { type: 'string', pattern: '^http://example\\d?\\.com/[\\w\\W]*$', patternFlags: 'i' } }, required: ['url'] }, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/'}}, {expected: true, context: {depth: 0, url: 'http://example1.com/'}}, {expected: true, context: {depth: 0, url: 'http://example2.com/'}}, {expected: true, context: {depth: 0, url: 'http://example3.com/'}}, {expected: true, context: {depth: 0, url: 'http://example.com/example'}}, {expected: false, context: {depth: 0, url: 'http://example.com:1234/'}}, {expected: false, context: {depth: 0, url: 'http://user@example.com:1234/'}}, {expected: false, context: {depth: 0, url: 'http://example-1.com/'}} ] }, // modifierKeys tests { conditionGroups: [ { conditions: [ { type: 'modifierKeys', operator: 'are', value: '' } ] } ], expectedSchema: { properties: { modifierKeys: { type: 'array', maxItems: 0, minItems: 0 } }, required: ['modifierKeys'] }, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} ] }, { conditionGroups: [ { conditions: [ { type: 'modifierKeys', operator: 'are', value: 'alt, shift' } ] } ], expectedSchema: { properties: { modifierKeys: { type: 'array', maxItems: 2, minItems: 2, allOf: [ {contains: {const: 'alt'}}, {contains: {const: 'shift'}} ] } }, required: ['modifierKeys'] }, inputs: [ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} ] }, { conditionGroups: [ { conditions: [ { type: 'modifierKeys', operator: 'areNot', value: '' } ] } ], expectedSchema: { not: { anyOf: [ { properties: { modifierKeys: { type: 'array', maxItems: 0, minItems: 0 } }, required: ['modifierKeys'] } ] } }, inputs: [ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} ] }, { conditionGroups: [ { conditions: [ { type: 'modifierKeys', operator: 'areNot', value: 'alt, shift' } ] } ], expectedSchema: { not: { anyOf: [ { properties: { modifierKeys: { type: 'array', maxItems: 2, minItems: 2, allOf: [ {contains: {const: 'alt'}}, {contains: {const: 'shift'}} ] } }, required: ['modifierKeys'] } ] } }, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} ] }, { conditionGroups: [ { conditions: [ { type: 'modifierKeys', operator: 'include', value: '' } ] } ], expectedSchema: { properties: { modifierKeys: { type: 'array', minItems: 0 } }, required: ['modifierKeys'] }, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} ] }, { conditionGroups: [ { conditions: [ { type: 'modifierKeys', operator: 'include', value: 'alt, shift' } ] } ], expectedSchema: { properties: { modifierKeys: { type: 'array', minItems: 2, allOf: [ {contains: {const: 'alt'}}, {contains: {const: 'shift'}} ] } }, required: ['modifierKeys'] }, inputs: [ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} ] }, { conditionGroups: [ { conditions: [ { type: 'modifierKeys', operator: 'notInclude', value: '' } ] } ], expectedSchema: { properties: { modifierKeys: { type: 'array' } }, required: ['modifierKeys'] }, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} ] }, { conditionGroups: [ { conditions: [ { type: 'modifierKeys', operator: 'notInclude', value: 'alt, shift' } ] } ], expectedSchema: { properties: { modifierKeys: { type: 'array', not: { anyOf: [ {contains: {const: 'alt'}}, {contains: {const: 'shift'}} ] } } }, required: ['modifierKeys'] }, inputs: [ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} ] }, // Flags tests { conditionGroups: [ { conditions: [ { type: 'flags', operator: 'are', value: '' } ] } ], expectedSchema: { required: ['flags'], properties: { flags: { type: 'array', maxItems: 0, minItems: 0 } } }, inputs: [ {expected: true, context: {depth: 0, url: ''}}, {expected: true, context: {depth: 0, url: '', flags: []}}, {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, { conditionGroups: [ { conditions: [ { type: 'flags', operator: 'are', value: 'clipboard, test2' } ] } ], expectedSchema: { required: ['flags'], properties: { flags: { type: 'array', maxItems: 2, minItems: 2, allOf: [ {contains: {const: 'clipboard'}}, {contains: {const: 'test2'}} ] } } }, inputs: [ {expected: false, context: {depth: 0, url: ''}}, {expected: false, context: {depth: 0, url: '', flags: []}}, {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, { conditionGroups: [ { conditions: [ { type: 'flags', operator: 'areNot', value: '' } ] } ], expectedSchema: { not: { anyOf: [ { required: ['flags'], properties: { flags: { type: 'array', maxItems: 0, minItems: 0 } } } ] } }, inputs: [ {expected: false, context: {depth: 0, url: ''}}, {expected: false, context: {depth: 0, url: '', flags: []}}, {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, { conditionGroups: [ { conditions: [ { type: 'flags', operator: 'areNot', value: 'clipboard, test2' } ] } ], expectedSchema: { not: { anyOf: [ { required: ['flags'], properties: { flags: { type: 'array', maxItems: 2, minItems: 2, allOf: [ {contains: {const: 'clipboard'}}, {contains: {const: 'test2'}} ] } } } ] } }, inputs: [ {expected: true, context: {depth: 0, url: ''}}, {expected: true, context: {depth: 0, url: '', flags: []}}, {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, { conditionGroups: [ { conditions: [ { type: 'flags', operator: 'include', value: '' } ] } ], expectedSchema: { required: ['flags'], properties: { flags: { type: 'array', minItems: 0 } } }, inputs: [ {expected: true, context: {depth: 0, url: ''}}, {expected: true, context: {depth: 0, url: '', flags: []}}, {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, { conditionGroups: [ { conditions: [ { type: 'flags', operator: 'include', value: 'clipboard, test2' } ] } ], expectedSchema: { required: ['flags'], properties: { flags: { type: 'array', minItems: 2, allOf: [ {contains: {const: 'clipboard'}}, {contains: {const: 'test2'}} ] } } }, inputs: [ {expected: false, context: {depth: 0, url: ''}}, {expected: false, context: {depth: 0, url: '', flags: []}}, {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, { conditionGroups: [ { conditions: [ { type: 'flags', operator: 'notInclude', value: '' } ] } ], expectedSchema: { required: ['flags'], properties: { flags: { type: 'array' } } }, inputs: [ {expected: true, context: {depth: 0, url: ''}}, {expected: true, context: {depth: 0, url: '', flags: []}}, {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, { conditionGroups: [ { conditions: [ { type: 'flags', operator: 'notInclude', value: 'clipboard, test2' } ] } ], expectedSchema: { required: ['flags'], properties: { flags: { type: 'array', not: { anyOf: [ {contains: {const: 'clipboard'}}, {contains: {const: 'test2'}} ] } } } }, inputs: [ {expected: true, context: {depth: 0, url: ''}}, {expected: true, context: {depth: 0, url: '', flags: []}}, {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, // Multiple conditions tests { conditionGroups: [ { conditions: [ { type: 'popupLevel', operator: 'greaterThan', value: '0' }, { type: 'popupLevel', operator: 'lessThan', value: '3' } ] } ], expectedSchema: { allOf: [ { properties: { depth: { type: 'number', exclusiveMinimum: 0 } }, required: ['depth'] }, { properties: { depth: { type: 'number', exclusiveMaximum: 3 } }, required: ['depth'] } ] }, inputs: [ {expected: false, context: {depth: -2, url: 'http://example.com/'}}, {expected: false, context: {depth: -1, url: 'http://example.com/'}}, {expected: false, context: {depth: 0, url: 'http://example.com/'}}, {expected: true, context: {depth: 1, url: 'http://example.com/'}}, {expected: true, context: {depth: 2, url: 'http://example.com/'}}, {expected: false, context: {depth: 3, url: 'http://example.com/'}} ] }, { conditionGroups: [ { conditions: [ { type: 'popupLevel', operator: 'greaterThan', value: '0' }, { type: 'popupLevel', operator: 'lessThan', value: '3' } ] }, { conditions: [ { type: 'popupLevel', operator: 'equal', value: '0' } ] } ], expectedSchema: { anyOf: [ { allOf: [ { properties: { depth: { type: 'number', exclusiveMinimum: 0 } }, required: ['depth'] }, { properties: { depth: { type: 'number', exclusiveMaximum: 3 } }, required: ['depth'] } ] }, { properties: { depth: {const: 0} }, required: ['depth'] } ] }, inputs: [ {expected: false, context: {depth: -2, url: 'http://example.com/'}}, {expected: false, context: {depth: -1, url: 'http://example.com/'}}, {expected: true, context: {depth: 0, url: 'http://example.com/'}}, {expected: true, context: {depth: 1, url: 'http://example.com/'}}, {expected: true, context: {depth: 2, url: 'http://example.com/'}}, {expected: false, context: {depth: 3, url: 'http://example.com/'}} ] }, { conditionGroups: [ { conditions: [ { type: 'popupLevel', operator: 'greaterThan', value: '0' }, { type: 'popupLevel', operator: 'lessThan', value: '3' } ] }, { conditions: [ { type: 'popupLevel', operator: 'lessThanOrEqual', value: '0' }, { type: 'popupLevel', operator: 'greaterThanOrEqual', value: '-1' } ] } ], expectedSchema: { anyOf: [ { allOf: [ { properties: { depth: { type: 'number', exclusiveMinimum: 0 } }, required: ['depth'] }, { properties: { depth: { type: 'number', exclusiveMaximum: 3 } }, required: ['depth'] } ] }, { allOf: [ { properties: { depth: { type: 'number', maximum: 0 } }, required: ['depth'] }, { properties: { depth: { type: 'number', minimum: -1 } }, required: ['depth'] } ] } ] }, inputs: [ {expected: false, context: {depth: -2, url: 'http://example.com/'}}, {expected: true, context: {depth: -1, url: 'http://example.com/'}}, {expected: true, context: {depth: 0, url: 'http://example.com/'}}, {expected: true, context: {depth: 1, url: 'http://example.com/'}}, {expected: true, context: {depth: 2, url: 'http://example.com/'}}, {expected: false, context: {depth: 3, url: 'http://example.com/'}} ] } ]; /* eslint-enable @stylistic/no-multi-spaces */ test.each(data)('schemas-test-%#', ({conditionGroups, expectedSchema, inputs}) => { const schema = createSchema(conditionGroups); if (typeof expectedSchema !== 'undefined') { expect(schema.schema).toStrictEqual(expectedSchema); } if (Array.isArray(inputs)) { for (const {expected, context} of inputs) { const normalizedContext = normalizeContext(context); const actual = schema.isValid(normalizedContext); expect(actual).toStrictEqual(expected); } } }); }); });