diff --git a/goml-script.md b/goml-script.md index f1279b1e..36625b4a 100644 --- a/goml-script.md +++ b/goml-script.md @@ -199,7 +199,7 @@ assert-attribute: ("#id > .class", {"attribute-name": "attribute-value"}, ALL) assert-attribute: ("//*[@id='id']/*[@class='class']", {"key1": "value1", "key2": "value2"}, ALL) ``` -You can use more specific checks as well by using one of the following identifiers: "ALL", "CONTAINS", "ENDS_WITH" or "STARTS_WITH". +You can use more specific checks as well by using one of the following identifiers: "ALL", "CONTAINS", "ENDS_WITH", "STARTS_WITH", or "NEAR". ``` assert-attribute: ( @@ -235,7 +235,7 @@ assert-attribute-false: ("#id > .class", {"attribute-name": "attribute-value"}, assert-attribute-false: ("//*[@id='id']/*[@class='class']", {"key1": "value1", "key2": "value2"}, ALL) ``` -You can use more specific checks as well by using one of the following identifiers: "ALL", "CONTAINS", "ENDS_WITH" or "STARTS_WITH". +You can use more specific checks as well by using one of the following identifiers: "ALL", "CONTAINS", "ENDS_WITH", "STARTS_WITH", or "NEAR". ``` assert-attribute-false: ( @@ -321,7 +321,7 @@ assert-document-property: ({"URL": "https://some.where", "title": "a title"}) assert-document-property: {"URL": "https://some.where", "title": "a title"} ``` -You can use more specific checks as well by using one of the following identifiers: "CONTAINS", "ENDS_WITH" or "STARTS_WITH". +You can use more specific checks as well by using one of the following identifiers: "CONTAINS", "ENDS_WITH", "STARTS_WITH", or "NEAR". ``` assert-document-property: ({"URL": "https://some.where", "title": "a title"}, STARTS_WITH) @@ -343,7 +343,8 @@ assert-document-property-false: ({"URL": "https://some.where", "title": "a title assert-document-property-false: {"URL": "https://some.where", "title": "a title"} ``` -You can use more specific checks as well by using one of the following identifiers: "CONTAINS", "ENDS_WITH" or "STARTS_WITH". +You can use more specific checks as well by using one of the following identifiers: "CONTAINS", "ENDS_WITH", +"STARTS_WITH" or "NEAR". ``` assert-document-property-false: ({"URL": "https://some.where", "title": "a title"}, STARTS_WITH) @@ -422,7 +423,7 @@ assert-property: ("#id > .class", { "offsetParent": "null" }, ALL) assert-property: ("//*[@id='id']/*[@class='class']", { "offsetParent": "null", "clientTop": "10px" }, ALL) ``` -You can use more specific checks as well by using one of the following identifiers: "ALL", "CONTAINS", "ENDS_WITH" or "STARTS_WITH". +You can use more specific checks as well by using one of the following identifiers: "ALL", "CONTAINS", "ENDS_WITH", "STARTS_WITH" or "NEAR". ``` assert-property: ( @@ -457,7 +458,7 @@ assert-property-false: ("#id > .class", { "offsetParent": "null" }, ALL) assert-property-false: ("//*[@id='id']/*[@class='class']", { "offsetParent": "null", "clientTop": "10px" }, ALL) ``` -You can use more specific checks as well by using one of the following identifiers: "ALL", "CONTAINS", "ENDS_WITH" or "STARTS_WITH". +You can use more specific checks as well by using one of the following identifiers: "ALL", "CONTAINS", "ENDS_WITH", "STARTS_WITH", or "NEAR". ``` assert-property-false: ( @@ -544,12 +545,27 @@ assert-variable: (variable_name, 12) assert-variable: (variable_name, 12.1) ``` -Apart from "CONTAINS", you can also use "ENDS_WITH" and "STARTS_WITH" and even combine them if you want. Example: +Apart from "CONTAINS", you can also use "ENDS_WITH", "STARTS_WITH" or "NEAR" and even combine them if you want. Example: ``` assert-variable: (variable_name, "hel", [CONTAINS, STARTS_WITH]) ``` +The `ENDS_WITH` and `STARTS_WITH` interpret the variable as a string, while `NEAR` interprets it as a number and asserts that the difference between the variable and the tested value is less than or equal to 1. This check is useful in cases where the browser rounds a potentially-fractional value to an integer, and may not always do it consistently from run to run: + +``` +// all of these assertions will pass +store-value: (variable_name, "hello") +assert-variable: (variable_name, "he", STARTS_WITH) +assert-variable: (variable_name, "o", ENDS_WITH) +store-value: (variable_name, 10) +assert-variable: (variable_name, 10, NEAR) +assert-variable: (variable_name, 9, NEAR) +assert-variable: (variable_name, 11, NEAR) +assert-variable-false: (variable_name, 8, NEAR) +assert-variable-false: (variable_name, 12, NEAR) +``` + For more information about variables, read the [variables section](#variables). #### assert-variable-false @@ -563,7 +579,7 @@ assert-variable-false: (variable_name, 12) assert-variable-false: (variable_name, 12.1) ``` -Apart from "CONTAINS", you can also use "ENDS_WITH" and "STARTS_WITH" and even combine them if you want. Example: +Apart from "CONTAINS", you can also use "ENDS_WITH", "STARTS_WITH" or "NEAR" and even combine them if you want. Example: ``` assert-variable-false: (variable_name, "hel", [CONTAINS, ENDS_WITH]) @@ -581,7 +597,7 @@ assert-window-property: ({"pageYOffset": "0", "location": "https://some.where"}) assert-window-property: {"pageYOffset": "0", "location": "https://some.where"} ``` -You can use more specific checks as well by using one of the following identifiers: "CONTAINS", "ENDS_WITH" or "STARTS_WITH". +You can use more specific checks as well by using one of the following identifiers: "CONTAINS", "ENDS_WITH", "STARTS_WITH", or "NEAR". ``` assert-window-property: ( @@ -609,7 +625,7 @@ assert-window-property-false: ({"location": "https://some.where", "pageYOffset": assert-window-property-false: {"location": "https://some.where", "pageYOffset": "10"} ``` -You can use more specific checks as well by using one of the following identifiers: "CONTAINS", "ENDS_WITH" or "STARTS_WITH". +You can use more specific checks as well by using one of the following identifiers: "CONTAINS", "ENDS_WITH", "STARTS_WITH" or "NEAR". ``` assert-window-property-false: ( diff --git a/src/commands/assert.js b/src/commands/assert.js index a17d76c8..a30e6376 100644 --- a/src/commands/assert.js +++ b/src/commands/assert.js @@ -137,7 +137,7 @@ function parseAssertCssFalse(parser) { function parseAssertObjPropertyInner(parser, assertFalse, objName) { const elems = parser.elems; - const identifiers = ['CONTAINS', 'ENDS_WITH', 'STARTS_WITH']; + const identifiers = ['CONTAINS', 'ENDS_WITH', 'STARTS_WITH', 'NEAR']; if (elems.length === 0) { return {'error': 'expected a tuple or a JSON dict, found nothing'}; @@ -254,6 +254,27 @@ ENDS_WITH check)'); if (!String(${objName}[${varKey}]).endsWith(${varValue})) { nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + ${objName}[${varKey}] + '\ \`) does not end with \`' + ${varValue} + '\`'); +}`); + } + } + if (enabled_checks['NEAR']) { + if (assertFalse) { + checks.push(`\ +if (Number.isNaN(${objName}[${varKey}])) { + nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + ${objName}[${varKey}] + '\ +\`) is NaN'); +} else if (Math.abs(${objName}[${varKey}] - ${varValue}) <= 1) { + nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + ${objName}[${varKey}] + '\ +\`) is within 1 of \`' + ${varValue} + '\`'); +}`); + } else { + checks.push(`\ +if (Number.isNaN(${objName}[${varKey}])) { + nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + ${objName}[${varKey}] + '\ +\`) is NaN'); +} else if (Math.abs(${objName}[${varKey}] - ${varValue}) > 1) { + nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + ${objName}[${varKey}] + '\ +\`) is not within 1 of \`' + ${varValue} + '\`'); }`); } } @@ -308,8 +329,8 @@ ${indentString(checks.join('\n'), 2)} // // * {"DOM property": "value"} // * ({"DOM property": "value"}) -// * ({"DOM property": "value"}, CONTAINS|ENDS_WITH|STARTS_WITH) -// * ({"DOM property": "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH]) +// * ({"DOM property": "value"}, CONTAINS|ENDS_WITH|STARTS_WITH|NEAR) +// * ({"DOM property": "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH|NEAR]) function parseAssertDocumentProperty(parser) { return parseAssertObjPropertyInner(parser, false, 'document'); } @@ -318,8 +339,8 @@ function parseAssertDocumentProperty(parser) { // // * {"DOM property": "value"} // * ({"DOM property": "value"}) -// * ({"DOM property": "value"}, CONTAINS|ENDS_WITH|STARTS_WITH) -// * ({"DOM property": "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH]) +// * ({"DOM property": "value"}, CONTAINS|ENDS_WITH|STARTS_WITH|NEAR) +// * ({"DOM property": "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH|NEAR]) function parseAssertDocumentPropertyFalse(parser) { return parseAssertObjPropertyInner(parser, true, 'document'); } @@ -328,8 +349,8 @@ function parseAssertDocumentPropertyFalse(parser) { // // * {"DOM property": "value"} // * ({"DOM property": "value"}) -// * ({"DOM property": "value"}, CONTAINS|ENDS_WITH|STARTS_WITH) -// * ({"DOM property": "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH]) +// * ({"DOM property": "value"}, CONTAINS|ENDS_WITH|STARTS_WITH|NEAR) +// * ({"DOM property": "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH|NEAR]) function parseAssertWindowProperty(parser) { return parseAssertObjPropertyInner(parser, false, 'window'); } @@ -338,8 +359,8 @@ function parseAssertWindowProperty(parser) { // // * {"DOM property": "value"} // * ({"DOM property": "value"}) -// * ({"DOM property": "value"}, CONTAINS|ENDS_WITH|STARTS_WITH) -// * ({"DOM property": "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH]) +// * ({"DOM property": "value"}, CONTAINS|ENDS_WITH|STARTS_WITH|NEAR) +// * ({"DOM property": "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH|NEAR]) function parseAssertWindowPropertyFalse(parser) { return parseAssertObjPropertyInner(parser, true, 'window'); } @@ -347,7 +368,7 @@ function parseAssertWindowPropertyFalse(parser) { function parseAssertPropertyInner(parser, assertFalse) { const err = 'Read the documentation to see the accepted inputs'; const elems = parser.elems; - const identifiers = ['ALL', 'CONTAINS', 'STARTS_WITH', 'ENDS_WITH']; + const identifiers = ['ALL', 'CONTAINS', 'STARTS_WITH', 'ENDS_WITH', 'NEAR']; const warnings = []; const enabled_checks = Object.create(null); @@ -432,6 +453,27 @@ ENDS_WITH check)'); if (!String(e[${varKey}]).endsWith(${varValue})) { nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + e[${varKey}] + '\ \`) does not end with \`' + ${varValue} + '\`'); +}`); + } + } + if (enabled_checks['NEAR']) { + if (assertFalse) { + checks.push(`\ +if (Number.isNaN(e[${varKey}])) { + nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + e[${varKey}] + '\ +\`) is NaN'); +} else if (Math.abs(e[${varKey}] - ${varValue}) <= 1) { + nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + e[${varKey}] + '\ +\`) is within 1 of \`' + ${varValue} + '\`'); +}`); + } else { + checks.push(`\ +if (Number.isNaN(e[${varKey}])) { + nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + e[${varKey}] + '\ +\`) is NaN'); +} else if (Math.abs(e[${varKey}] - ${varValue}) > 1) { + nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + e[${varKey}] + '\ +\`) is not within 1 of \`' + ${varValue} + '\`'); }`); } } @@ -544,7 +586,7 @@ function parseAssertPropertyFalse(parser) { function parseAssertAttributeInner(parser, assertFalse) { const err = 'Read the documentation to see the accepted inputs'; const elems = parser.elems; - const identifiers = ['ALL', 'CONTAINS', 'STARTS_WITH', 'ENDS_WITH']; + const identifiers = ['ALL', 'CONTAINS', 'STARTS_WITH', 'ENDS_WITH', 'NEAR']; const warnings = []; const enabled_checks = Object.create(null); @@ -629,6 +671,27 @@ if (attr.endsWith(${varValue})) { if (!attr.endsWith(${varValue})) { nonMatchingAttrs.push("attribute \`" + ${varKey} + "\` (\`" + attr + "\`) doesn't end with \`"\ + ${varValue} + "\`"); +}`); + } + } + if (enabled_checks['NEAR']) { + if (assertFalse) { + checks.push(`\ +if (Number.isNaN(attr)) { + nonMatchingProps.push('attribute \`' + ${varKey} + '\` (\`' + attr + '\ +\`) is NaN'); +} else if (Math.abs(attr] - ${varValue}) <= 1) { + nonMatchingProps.push('attribute \`' + ${varKey} + '\` (\`' + attr + '\ +\`) is within 1 of \`' + ${varValue} '\`'); +}`); + } else { + checks.push(`\ +if (Number.isNaN(attr)) { + nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + attr + '\ +\`) is NaN'); +} else if (Math.abs(attr - ${varValue}) > 1) { + nonMatchingProps.push('Property \`' + ${varKey} + '\` (\`' + attr + '\ +\`) is not within 1 of \`' + ${varValue} '\`'); }`); } } @@ -1264,7 +1327,7 @@ function parseAssertVariableInner(parser, assertFalse) { ${tuple[1].getArticleKind()} (\`${tuple[1].getErrorText()}\`)`, }; } else if (tuple.length > 2) { - const identifiers = ['CONTAINS', 'STARTS_WITH', 'ENDS_WITH']; + const identifiers = ['CONTAINS', 'STARTS_WITH', 'ENDS_WITH', 'NEAR']; const ret = fillEnabledChecks(tuple[2], identifiers, enabled_checks, warnings, 'third'); if (ret !== null) { return ret; @@ -1309,6 +1372,23 @@ if (value1.endsWith(value2)) { checks.push(`\ if (!value1.endsWith(value2)) { errors.push("\`" + value1 + "\` doesn't end with \`" + value2 + "\` (for ENDS_WITH check)"); +}`); + } + } + if (enabled_checks['NEAR']) { + if (assertFalse) { + checks.push(`\ +if (Number.isNaN(value1])) { + nonMatchingProps.push('\`' + value1 + '\` is NaN'); +} else if (Math.abs(value1 - value2) <= 1) { + nonMatchingProps.push('\`' + value1 + '\` is within 1 of \`' + value2 '\`'); +}`); + } else { + checks.push(`\ +if (Number.isNaN(value1])) { + nonMatchingProps.push('\`' + value1 + '\` is NaN'); +} else if (Math.abs(value1 - value2) > 1) { + nonMatchingProps.push('\`' + value1 + '\` is not within 1 of \`' + value2 '\`'); }`); } } diff --git a/tests/scripts/assert-document-property.goml b/tests/scripts/assert-document-property.goml index aa5178a5..a3afeee0 100644 --- a/tests/scripts/assert-document-property.goml +++ b/tests/scripts/assert-document-property.goml @@ -11,3 +11,10 @@ assert-document-property-false: ({"title": "bsic"}, STARTS_WITH) assert-document-property-false: ({"title": "bsic"}, CONTAINS) assert-document-property-false: ({"title": "bsic"}, ENDS_WITH) assert-document-property-false: ({"title": "bsic"}, [STARTS_WITH, CONTAINS]) +assert-document-property: ({"nodeType": 9}) +assert-document-property-false: ({"nodeType": 8}) +assert-document-property: ({"nodeType": 9}, NEAR) +assert-document-property: ({"nodeType": 8}, NEAR) +assert-document-property-false: ({"nodeType": 7}, NEAR) +assert-document-property: ({"nodeType": 10}, NEAR) +assert-document-property-false: ({"nodeType": 11}, NEAR) diff --git a/tests/scripts/assert.goml b/tests/scripts/assert.goml index 07dee639..b27f1353 100644 --- a/tests/scripts/assert.goml +++ b/tests/scripts/assert.goml @@ -7,3 +7,8 @@ assert-count: ("div", 1) assert-css: ("#button", {"padding": "5px"}) assert-attribute: ("#button", {"href": "./other_page.html"}) assert-property: ("#button", {"offsetParent": "null"}) +assert-property: ("body", {"offsetTop": 0}) +assert-property-false: ("body", {"offsetTop": 1}) +assert-property: ("body", {"offsetTop": 0}, NEAR) +assert-property: ("body", {"offsetTop": 1}, NEAR) +assert-property-false: ("body", {"offsetTop": 2}, NEAR) diff --git a/tests/scripts/attribute.goml b/tests/scripts/attribute.goml index 306f1fe5..13470815 100644 --- a/tests/scripts/attribute.goml +++ b/tests/scripts/attribute.goml @@ -12,3 +12,12 @@ assert-attribute: ("#yet-another-id", {"\"e": "'2"}) fail: false attribute: ("#yet-another-id", "e", "'2") assert-attribute: ("#yet-another-id", {"e": "'2"}) +attribute: ("#yet-another-id", "e", "2") +assert-attribute: ("#yet-another-id", {"e": "2"}) +assert-attribute-false: ("#yet-another-id", {"e": "3"}) +assert-attribute: ("#yet-another-id", {"e": "2"}, NEAR) +assert-attribute: ("#yet-another-id", {"e": "3"}, NEAR) +assert-attribute: ("#yet-another-id", {"e": "1"}, NEAR) +fail: true +assert-attribute: ("#yet-another-id", {"e": "4"}, NEAR) +assert-attribute: ("#yet-another-id", {"e": "0"}, NEAR) diff --git a/tests/test-js/api.js b/tests/test-js/api.js index df05b9b9..242424ba 100644 --- a/tests/test-js/api.js +++ b/tests/test-js/api.js @@ -146,11 +146,11 @@ function checkAssertAttributeInner(x, func, notFound, equal, contains, startsWit }); x.assert(func('("a::after", {"a": 1}, all)'), { 'error': 'unknown identifier `all`. Available identifiers are: [`ALL`, `CONTAINS`, ' + - '`STARTS_WITH`, `ENDS_WITH`]', + '`STARTS_WITH`, `ENDS_WITH`, `NEAR`]', }); x.assert(func('("a::after", {"a": 1}, ALLO)'), { 'error': 'unknown identifier `ALLO`. Available identifiers are: [`ALL`, `CONTAINS`, ' + - '`STARTS_WITH`, `ENDS_WITH`]', + '`STARTS_WITH`, `ENDS_WITH`, `NEAR`]', }); x.assert(func('("a", {"b": "c", "b": "d"})'), {'error': 'attribute `b` is duplicated'}); @@ -667,7 +667,7 @@ ${equal(3)} // Multiline x.assert(func('("a::after", \n {"a": 1}, \n ALLO)'), { 'error': 'unknown identifier `ALLO`. Available identifiers are: [`ALL`, `CONTAINS`, ' + - '`STARTS_WITH`, `ENDS_WITH`]', + '`STARTS_WITH`, `ENDS_WITH`, `NEAR`]', }); x.assert(func('("//a",\n \n{"b": "c"}, \n ALL)'), { 'instructions': [`\ @@ -878,7 +878,7 @@ function checkAssertObjPropertyInner( x.assert(func('("a", "b" "c", ALL)'), {'error': 'expected `,` or `)`, found `"` after `"b"`'}); x.assert(func('({"a": "b"}, all)'), { 'error': 'unknown identifier `all`. Available identifiers are: [`CONTAINS`, `ENDS_WITH`, ' + - '`STARTS_WITH`]', + '`STARTS_WITH`, `NEAR`]', }); x.assert(func('("a::after", {"a": 1}, ALLO)'), { 'error': 'expected a tuple of one or two elements, found 3 elements', @@ -2186,11 +2186,11 @@ function checkAssertPropertyInner(x, func, exists, equal, startsWith, endsWith) }); x.assert(func('("a::after", {"a": 1}, all)'), { 'error': 'unknown identifier `all`. Available identifiers are: [`ALL`, `CONTAINS`, ' + - '`STARTS_WITH`, `ENDS_WITH`]', + '`STARTS_WITH`, `ENDS_WITH`, `NEAR`]', }); x.assert(func('("a::after", {"a": 1}, ALLO)'), { 'error': 'unknown identifier `ALLO`. Available identifiers are: [`ALL`, `CONTAINS`, ' + - '`STARTS_WITH`, `ENDS_WITH`]', + '`STARTS_WITH`, `ENDS_WITH`, `NEAR`]', }); x.assert(func('("a", {"b": "c", "b": "d"})'), {'error': 'property `b` is duplicated'}); x.assert(func('("a", {"b": []})'), {