diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2020-08-15 17:22:23 -0400 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-08-15 17:22:23 -0400 | 
| commit | d8649f40d59356361ce470cc220dca6c62a66388 (patch) | |
| tree | 15f4fe00721375aa77afe2e840d22e85b10dace3 | |
| parent | 587822c16e3f573362fdfe291c9afc37ca31bb15 (diff) | |
JSON-schema-based profile conditions (#730)
* Add ProfileConditions class
* Add URL to VM
* Add new ProfileConditions tests
| -rw-r--r-- | ext/bg/js/profile-conditions2.js | 276 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | test/test-profile-conditions.js | 847 | ||||
| -rw-r--r-- | test/yomichan-vm.js | 21 | 
4 files changed, 1145 insertions, 1 deletions
| diff --git a/ext/bg/js/profile-conditions2.js b/ext/bg/js/profile-conditions2.js new file mode 100644 index 00000000..9f2f6b16 --- /dev/null +++ b/ext/bg/js/profile-conditions2.js @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2020  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/>. + */ + +/** + * Utility class to help processing profile conditions. + */ +class ProfileConditions { +    /** +     * Creates a new instance. +     */ +    constructor() { +        this._splitPattern = /[,;\s]+/; +        this._descriptors = new Map([ +            [ +                'popupLevel', +                { +                    operators: new Map([ +                        ['equal',              this._createSchemaPopupLevelEqual.bind(this)], +                        ['notEqual',           this._createSchemaPopupLevelNotEqual.bind(this)], +                        ['lessThan',           this._createSchemaPopupLevelLessThan.bind(this)], +                        ['greaterThan',        this._createSchemaPopupLevelGreaterThan.bind(this)], +                        ['lessThanOrEqual',    this._createSchemaPopupLevelLessThanOrEqual.bind(this)], +                        ['greaterThanOrEqual', this._createSchemaPopupLevelGreaterThanOrEqual.bind(this)] +                    ]) +                } +            ], +            [ +                'url', +                { +                    operators: new Map([ +                        ['matchDomain', this._createSchemaUrlMatchDomain.bind(this)], +                        ['matchRegExp', this._createSchemaUrlMatchRegExp.bind(this)] +                    ]) +                } +            ], +            [ +                'modifierKeys', +                { +                    operators: new Map([ +                        ['are', this._createSchemaModifierKeysAre.bind(this)], +                        ['areNot', this._createSchemaModifierKeysAreNot.bind(this)], +                        ['include', this._createSchemaModifierKeysInclude.bind(this)], +                        ['notInclude', this._createSchemaModifierKeysNotInclude.bind(this)] +                    ]) +                } +            ] +        ]); +    } + +    /** +     * Creates a new JSON schema descriptor for the given set of condition groups. +     * @param conditionGroups An array of condition groups in the following format: +     *  conditionGroups = [ +     *      { +     *          conditions: [ +     *              { +     *                  type: (condition type: string), +     *                  operator: (condition sub-type: string), +     *                  value: (value to compare against: string) +     *              }, +     *              ... +     *          ] +     *      }, +     *      ... +     *  ] +     */ +    createSchema(conditionGroups) { +        const anyOf = []; +        for (const {conditions} of conditionGroups) { +            const allOf = []; +            for (const {type, operator, value} of conditions) { +                const conditionDescriptor = this._descriptors.get(type); +                if (typeof conditionDescriptor === 'undefined') { continue; } + +                const createSchema = conditionDescriptor.operators.get(operator); +                if (typeof createSchema === 'undefined') { continue; } + +                const schema = createSchema(value); +                allOf.push(schema); +            } +            switch (allOf.length) { +                case 0: break; +                case 1: anyOf.push(allOf[0]); break; +                default: anyOf.push({allOf}); break; +            } +        } +        switch (anyOf.length) { +            case 0: return {}; +            case 1: return anyOf[0]; +            default: return {anyOf}; +        } +    } + +    /** +     * Creates a normalized version of the context object to test, +     * assigning dependent fields as needed. +     * @param context A context object which is used during schema validation. +     * @returns A normalized context object. +     */ +    normalizeContext(context) { +        const normalizedContext = Object.assign({}, context); +        const {url} = normalizedContext; +        if (typeof url === 'string') { +            try { +                normalizedContext.domain = new URL(url).hostname; +            } catch (e) { +                // NOP +            } +        } +        return normalizedContext; +    } + +    // Private + +    _split(value) { +        return value.split(this._splitPattern); +    } + +    _stringToNumber(value) { +        const number = Number.parseFloat(value); +        return Number.isFinite(number) ? number : 0; +    } + +    // popupLevel schema creation functions + +    _createSchemaPopupLevelEqual(value) { +        value = this._stringToNumber(value); +        return { +            required: ['depth'], +            properties: { +                depth: {const: value} +            } +        }; +    } + +    _createSchemaPopupLevelNotEqual(value) { +        return { +            not: [this._createSchemaPopupLevelEqual(value)] +        }; +    } + +    _createSchemaPopupLevelLessThan(value) { +        value = this._stringToNumber(value); +        return { +            required: ['depth'], +            properties: { +                depth: {type: 'number', exclusiveMaximum: value} +            } +        }; +    } + +    _createSchemaPopupLevelGreaterThan(value) { +        value = this._stringToNumber(value); +        return { +            required: ['depth'], +            properties: { +                depth: {type: 'number', exclusiveMinimum: value} +            } +        }; +    } + +    _createSchemaPopupLevelLessThanOrEqual(value) { +        value = this._stringToNumber(value); +        return { +            required: ['depth'], +            properties: { +                depth: {type: 'number', maximum: value} +            } +        }; +    } + +    _createSchemaPopupLevelGreaterThanOrEqual(value) { +        value = this._stringToNumber(value); +        return { +            required: ['depth'], +            properties: { +                depth: {type: 'number', minimum: value} +            } +        }; +    } + +    // url schema creation functions + +    _createSchemaUrlMatchDomain(value) { +        const oneOf = []; +        for (let domain of this._split(value)) { +            if (domain.length === 0) { continue; } +            domain = domain.toLowerCase(); +            oneOf.push({const: domain}); +        } +        return { +            required: ['domain'], +            properties: { +                domain: {oneOf} +            } +        }; +    } + +    _createSchemaUrlMatchRegExp(value) { +        return { +            required: ['url'], +            properties: { +                url: {type: 'string', pattern: value, patternFlags: 'i'} +            } +        }; +    } + +    // modifierKeys schema creation functions + +    _createSchemaModifierKeysAre(value) { +        return this._createSchemaModifierKeysGeneric(value, true, false); +    } + +    _createSchemaModifierKeysAreNot(value) { +        return { +            not: [this._createSchemaModifierKeysGeneric(value, true, false)] +        }; +    } + +    _createSchemaModifierKeysInclude(value) { +        return this._createSchemaModifierKeysGeneric(value, false, false); +    } + +    _createSchemaModifierKeysNotInclude(value) { +        return this._createSchemaModifierKeysGeneric(value, false, true); +    } + +    _createSchemaModifierKeysGeneric(value, exact, none) { +        const containsList = []; +        for (const modifierKey of this._split(value)) { +            if (modifierKey.length === 0) { continue; } +            containsList.push({ +                contains: { +                    const: modifierKey +                } +            }); +        } +        const containsListCount = containsList.length; +        const modifierKeysSchema = { +            type: 'array' +        }; +        if (exact) { +            modifierKeysSchema.maxItems = containsListCount; +        } +        if (none) { +            if (containsListCount > 0) { +                modifierKeysSchema.not = containsList; +            } +        } else { +            modifierKeysSchema.minItems = containsListCount; +            if (containsListCount > 0) { +                modifierKeysSchema.allOf = containsList; +            } +        } +        return { +            required: ['modifierKeys'], +            properties: { +                modifierKeys: modifierKeysSchema +            } +        }; +    } +} diff --git a/package.json b/package.json index 4a788980..8bbf883a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@          "build": "node ./dev/build.js",          "test": "npm run test-lint && npm run test-code && npm run test-manifest",          "test-lint": "eslint . && node ./test/lint/global-declarations.js", -        "test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-document-util.js && node ./test/test-object-property-accessor.js && node ./test/test-japanese.js && node ./test/test-text-source-map.js && node ./test/test-dom-text-scanner.js && node ./test/test-cache-map.js", +        "test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-document-util.js && node ./test/test-object-property-accessor.js && node ./test/test-japanese.js && node ./test/test-text-source-map.js && node ./test/test-dom-text-scanner.js && node ./test/test-cache-map.js && node ./test/test-profile-conditions.js",          "test-manifest": "node ./test/test-manifest.js"      },      "repository": { diff --git a/test/test-profile-conditions.js b/test/test-profile-conditions.js new file mode 100644 index 00000000..ce26cbb4 --- /dev/null +++ b/test/test-profile-conditions.js @@ -0,0 +1,847 @@ +/* + * Copyright (C) 2020  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/>. + */ + +const assert = require('assert'); +const {VM} = require('./yomichan-vm'); + + +const vm = new VM({}); +vm.execute([ +    'mixed/js/core.js', +    'mixed/js/cache-map.js', +    'bg/js/json-schema.js', +    'bg/js/profile-conditions2.js' +]); +const [JsonSchema, ProfileConditions] = vm.get(['JsonSchema', 'ProfileConditions']); + + +function schemaValidate(value, schema) { +    try { +        JsonSchema.validate(value, schema); +        return true; +    } catch (e) { +        return false; +    } +} + + +function testNormalizeContext() { +    const data = [ +        // Empty +        { +            context: {}, +            expected: {} +        }, + +        // Domain normalization +        { +            context: {url: ''}, +            expected: {url: ''} +        }, +        { +            context: {url: 'http://example.com/'}, +            expected: {url: 'http://example.com/', domain: 'example.com'} +        }, +        { +            context: {url: 'http://example.com:1234/'}, +            expected: {url: 'http://example.com:1234/', domain: 'example.com'} +        }, +        { +            context: {url: 'http://user@example.com:1234/'}, +            expected: {url: 'http://user@example.com:1234/', domain: 'example.com'} +        } +    ]; + +    for (const {context, expected} of data) { +        const profileConditions = new ProfileConditions(); +        const actual = profileConditions.normalizeContext(context); +        vm.assert.deepStrictEqual(actual, expected); +    } +} + +function testSchemas() { +    const data = [ +        // Empty +        { +            conditionGroups: [], +            expectedSchema: {}, +            inputs: [ +                {expected: true, context: {url: 'http://example.com/'}} +            ] +        }, +        { +            conditionGroups: [ +                {conditions: []} +            ], +            expectedSchema: {}, +            inputs: [ +                {expected: true, context: {url: 'http://example.com/'}} +            ] +        }, +        { +            conditionGroups: [ +                {conditions: []}, +                {conditions: []} +            ], +            expectedSchema: {}, +            inputs: [ +                {expected: true, context: {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: [ +                    { +                        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: [ +                    { +                        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: [ +                    { +                        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: [ +                            {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']}} +            ] +        }, + +        // 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/'}} +            ] +        } +    ]; + +    for (const {conditionGroups, expectedSchema, inputs} of data) { +        const profileConditions = new ProfileConditions(); +        const schema = profileConditions.createSchema(conditionGroups); +        if (typeof expectedSchema !== 'undefined') { +            vm.assert.deepStrictEqual(schema, expectedSchema); +        } +        if (Array.isArray(inputs)) { +            for (const {expected, context} of inputs) { +                const normalizedContext = profileConditions.normalizeContext(context); +                const actual = schemaValidate(normalizedContext, schema); +                assert.strictEqual(actual, expected); +            } +        } +    } +} + + +function main() { +    testNormalizeContext(); +    testSchemas(); +} + + +if (require.main === module) { main(); } diff --git a/test/yomichan-vm.js b/test/yomichan-vm.js index 97faa03e..79e92772 100644 --- a/test/yomichan-vm.js +++ b/test/yomichan-vm.js @@ -115,8 +115,29 @@ function deepStrictEqual(actual, expected) {  } +function createURLClass() { +    const BaseURL = URL; +    return function URL(url) { +        const u = new BaseURL(url); +        this.hash = u.hash; +        this.host = u.host; +        this.hostname = u.hostname; +        this.href = u.href; +        this.origin = u.origin; +        this.password = u.password; +        this.pathname = u.pathname; +        this.port = u.port; +        this.protocol = u.protocol; +        this.search = u.search; +        this.searchParams = u.searchParams; +        this.username = u.username; +    }; +} + +  class VM {      constructor(context={}) { +        context.URL = createURLClass();          this._context = vm.createContext(context);          this._assert = {              deepStrictEqual |