UNPKG

browser-ui-test

Version:

Small JS framework to provide headless browser-based tests

1,650 lines (1,522 loc) 47.7 kB
// All `assert*` commands. // FIXME: rename this `utils.js` into `command_utils.js`. const { fillEnabledChecksV2, getAndSetElements, getInsertStrings, indentString, makeExtendedChecks, makeTextExtendedChecks, validatePositionDictV2, commonPositionCheckCode, commonSizeCheckCode, generateCheckObjectPaths, checkClipboardPermission, } = require('./utils.js'); const { COLOR_CHECK_ERROR } = require('../consts.js'); const { cleanString } = require('../parser.js'); const { validator } = require('../validator.js'); // Not the same `utils.js`! const { hasError } = require('../utils.js'); function parseAssertCssInner(parser, assertFalse) { const jsonValidator = { kind: 'json', allowEmptyValues: false, keyTypes: { 'string': [], }, valueTypes: { 'string': {}, 'number': { allowNegative: true, allowFloat: true, }, }, }; const ret = validator(parser, { kind: 'tuple', elements: [ { kind: 'selector' }, jsonValidator, { kind: 'ident', allowed: ['ALL'], optional: true, }, ], }, ); if (hasError(ret)) { return ret; } const tuple = ret.value.entries; const checkAllElements = tuple.length > 2; const propertyDict = tuple[1].value.entries; const selector = tuple[0].value; const xpath = selector.isXPath ? 'XPath ' : 'selector '; const pseudo = !selector.isXPath && selector.pseudo !== null ? `, "${selector.pseudo}"` : ''; const varName = 'parseAssertElemCss'; const varDict = varName + 'Dict'; let extra; if (checkAllElements) { extra = `\ for (const [i, elem] of ${varName}.entries()) { await checkElem(elem, i); }`; } else { extra = `await checkElem(${varName});`; } let assertCheck; if (assertFalse) { assertCheck = `\ if (localErr.length === 0) { nonMatchingProps.push("assert didn't fail for key \`" + key + '\`'); }`; } else { assertCheck = 'nonMatchingProps.push(...localErr);'; } const instructions = []; if (propertyDict.has('color')) { instructions.push(`\ if (!arg.showText) { throw "${COLOR_CHECK_ERROR}"; }`); } let keys = ''; let values = ''; for (const [key, value] of propertyDict) { if (keys.length !== 0) { keys += ','; values += ','; } keys += `"${key}"`; values += `"${value.value}"`; } instructions.push(`\ const { checkCssProperty } = require('command-helpers.js'); async function checkElem(elem, i) { const nonMatchingProps = []; const jsHandle = await elem.evaluateHandle(e => { const ${varDict} = [${keys}]; const assertComputedStyle = window.getComputedStyle(e${pseudo}); const simple = []; const computed = []; const keys = []; for (const entry of ${varDict}) { simple.push(e.style[entry]); computed.push(assertComputedStyle[entry]); keys.push(entry); } return [keys, simple, computed]; }); const [keys, simple, computed] = await jsHandle.jsonValue(); const values = [${values}]; for (const [i, key] of keys.entries()) { const localErr = []; checkCssProperty(key, values[i], simple[i], computed[i], localErr); ${indentString(assertCheck, 3)} } if (nonMatchingProps.length !== 0) { const props = nonMatchingProps.join("; "); let err = "The following errors happened (for ${xpath}\`${selector.value}\`): [" + props \ + "]"; if (i !== undefined) { err += ' (on the element number ' + i + ')'; } throw err; } } ${getAndSetElements(selector, varName, checkAllElements)} ${extra}`); return { 'instructions': instructions, 'wait': false, 'checkResult': true, }; } // Possible inputs: // // * ("CSS", {"css property": "value"}) // * ("XPath", {"css property": "value"}) // * ("CSS", {"css property": "value"}, ALL) // * ("XPath", {"css property": "value"}, ALL) function parseAssertCss(parser) { return parseAssertCssInner(parser, false); } // Possible inputs: // // * ("CSS", {"css property": "value"}) // * ("XPath", {"css property": "value"}) // * ("CSS", {"css property": "value"}, ALL) // * ("XPath", {"css property": "value"}, ALL) function parseAssertCssFalse(parser) { return parseAssertCssInner(parser, true); } function parseAssertObjPropertyInner(parser, assertFalse, objName) { const identifiers = ['CONTAINS', 'ENDS_WITH', 'STARTS_WITH', 'NEAR']; const jsonValidator = { kind: 'json', keyTypes: { 'string': [], 'object-path': [], }, valueTypes: { string: {}, number: { allowNegative: true, allowFloat: true, }, boolean: {}, ident: { allowed: ['null'], }, }, }; const ret = validator(parser, { kind: 'tuple', elements: [ jsonValidator, { kind: 'ident', allowed: identifiers, optional: true, alternatives: [ { kind: 'array', valueTypes: { 'ident': { allowed: identifiers, }, }, }, ], }, ], alternatives: [jsonValidator], }, ); if (hasError(ret)) { return ret; } const enabledChecks = new Set(); const warnings = []; let json_dict; if (ret.kind === 'json') { json_dict = ret.value.entries; } else { const tuple = ret.value.entries; json_dict = tuple[0].value.entries; if (tuple.length > 1) { const ret = fillEnabledChecksV2( tuple[1], enabledChecks, warnings, 'second', ); if (ret !== null) { return ret; } } } if (json_dict.size === 0) { return { 'instructions': [], 'wait': false, 'warnings': warnings, 'checkResult': true, }; } const varName = 'parseAssertObj'; const varDict = varName + 'Dict'; const varKey = varName + 'Key'; const varValue = varName + 'Value'; // JSON.stringify produces a problematic output so instead we use this. const props = []; const undefProps = []; for (const [k, v] of json_dict) { const k_s = v.key.kind === 'object-path' ? k : `["${k}"]`; if (v.kind !== 'ident') { props.push(`[${k_s},"${v.value}"]`); } else { undefProps.push(k_s); } } if (props.length + undefProps.length === 0) { // Unlike when elements are involved, if there are no checks, we can return early // because we don't care whether or not the element exists. return { 'instructions': [], 'wait': false, }; } const { checks, hasSpecialChecks } = makeExtendedChecks( enabledChecks, assertFalse, 'nonMatchingProps', `${objName} property`, 'prop', varKey, varValue, ); if (undefProps.length > 0 && hasSpecialChecks) { const k = [...enabledChecks].join(', '); warnings.push(`Special checks (${k}) will be ignored for \`null\``); } let expectedPropError = ''; let unexpectedPropError = ''; let unknown = ''; if (!assertFalse) { unknown = ` const p = ${varKey}.map(p => \`"\${p}"\`).join('.'); nonMatchingProps.push('Unknown ${objName} property \`' + p + '\`');`; unexpectedPropError = `\ const p = prop.map(p => \`"\${p}"\`).join('.'); nonMatchingProps.push("Expected property \`" + p + "\` to not exist, found: \`" + val + "\`");`; } else { expectedPropError = ` const p = prop.map(p => \`"\${p}"\`).join('.'); nonMatchingProps.push("Property named \`" + p + "\` doesn't exist");`; } const instructions = [`\ await page.evaluate(() => { ${indentString(generateCheckObjectPaths(), 1)} const nonMatchingProps = []; const ${varDict} = [${props.join(',')}]; const undefProps = [${undefProps.join(',')}]; for (const prop of undefProps) { checkObjectPaths(${objName}, prop, val => { if (val !== undefined && val !== null) { ${indentString(unexpectedPropError, 4)} return; }${indentString(expectedPropError, 3)} }, prop => {${indentString(expectedPropError, 3)} }); } for (const [${varKey}, ${varValue}] of ${varDict}) { checkObjectPaths(${objName}, ${varKey}, val => { if (val === undefined || val === null) {${indentString(unknown, 4)} return; } const prop = String(val); ${indentString(checks.join('\n'), 3)} }, ${varKey} => {${indentString(unknown, 3)} }); } if (nonMatchingProps.length !== 0) { const props = nonMatchingProps.join("; "); throw "The following errors happened: [" + props + "]"; } });`, ]; return { 'instructions': instructions, 'wait': false, 'warnings': warnings, 'checkResult': true, }; } // Possible inputs: // // * {"DOM property"|object path: "value"} // * ({"DOM property"|object path: "value"}) // * ({"DOM property"|object path: "value"}, CONTAINS|ENDS_WITH|STARTS_WITH|NEAR) // * ({"DOM property"|object path: "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH|NEAR]) function parseAssertDocumentProperty(parser) { return parseAssertObjPropertyInner(parser, false, 'document'); } // Possible inputs: // // * {"DOM property"|object path: "value"} // * ({"DOM property"|object path: "value"}) // * ({"DOM property"|object path: "value"}, CONTAINS|ENDS_WITH|STARTS_WITH|NEAR) // * ({"DOM property"|object path: "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH|NEAR]) function parseAssertDocumentPropertyFalse(parser) { return parseAssertObjPropertyInner(parser, true, 'document'); } // Possible inputs: // // * {"DOM property"|object path: "value"} // * ({"DOM property"|object path: "value"}) // * ({"DOM property"|object path: "value"}, CONTAINS|ENDS_WITH|STARTS_WITH|NEAR) // * ({"DOM property"|object path: "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH|NEAR]) function parseAssertWindowProperty(parser) { return parseAssertObjPropertyInner(parser, false, 'window'); } // Possible inputs: // // * {"DOM property"|object path: "value"} // * ({"DOM property"|object path: "value"}) // * ({"DOM property"|object path: "value"}, CONTAINS|ENDS_WITH|STARTS_WITH|NEAR) // * ({"DOM property"|object path: "value"}, [CONTAINS|ENDS_WITH|STARTS_WITH|NEAR]) function parseAssertWindowPropertyFalse(parser) { return parseAssertObjPropertyInner(parser, true, 'window'); } function parseAssertPropertyInner(parser, assertFalse) { const identifiers = ['CONTAINS', 'ENDS_WITH', 'STARTS_WITH', 'NEAR', 'ALL']; const jsonValidator = { kind: 'json', keyTypes: { string: [], 'object-path': [], }, valueTypes: { string: {}, number: { allowNegative: true, allowFloat: true, }, ident: { allowed: ['null'], }, }, }; const selectorValidator = { kind: 'selector' }; const ret = validator(parser, { kind: 'tuple', elements: [ selectorValidator, jsonValidator, { kind: 'ident', allowed: identifiers, optional: true, alternatives: [ { kind: 'array', valueTypes: { 'ident': { allowed: identifiers, }, }, }, ], }, ], }, ); if (hasError(ret)) { return ret; } const tuple = ret.value.entries; const warnings = []; const enabledChecks = new Set(); if (tuple.length > 2) { fillEnabledChecksV2(tuple[2], enabledChecks, warnings, 'third'); } const selector = tuple[0].value; const xpath = selector.isXPath ? 'XPath ' : 'selector '; const varName = 'parseAssertElemProp'; const varDict = varName + 'Dict'; const varKey = varName + 'Key'; const varValue = varName + 'Value'; const { checks, hasSpecialChecks } = makeExtendedChecks( enabledChecks, assertFalse, 'nonMatchingProps', 'property', 'prop', `${varKey}.map(p => \`"\${p}"\`).join('.')`, varValue, ); const json = tuple[1].value.entries; const props = []; const undefProps = []; for (const [k, v] of json) { const k_s = v.key.kind === 'object-path' ? k : `["${k}"]`; if (v.kind !== 'ident') { props.push(`[${k_s},"${v.value}"]`); } else { undefProps.push(k_s); } } const isPseudo = !selector.isXPath && selector.pseudo !== null; if (isPseudo) { warnings.push(`Pseudo-elements (\`${selector.pseudo}\`) don't have properties so \ the check will be performed on the element itself`); } if (undefProps.length > 0 && hasSpecialChecks) { const k = [...enabledChecks].filter(k => k !== 'ALL').join(', '); warnings.push(`Special checks (${k}) will be ignored for \`null\``); } let expectedPropError = ''; let unexpectedPropError = ''; let unknown = ''; if (!assertFalse) { unknown = ` const p = ${varKey}.map(p => \`"\${p}"\`).join('.'); nonMatchingProps.push('Unknown property \`' + p + '\`');`; unexpectedPropError = `\ const p = prop.map(p => \`"\${p}"\`).join('.'); nonMatchingProps.push("Expected property \`" + p + "\` to not exist, found: \`" + val + "\`");`; } else { expectedPropError = ` const p = prop.map(p => \`"\${p}"\`).join('.'); nonMatchingProps.push("Property named \`" + p + "\` doesn't exist");`; } const checkAllElements = enabledChecks.has('ALL') === true; const indent = checkAllElements ? 1 : 0; let whole = getAndSetElements(selector, varName, checkAllElements) + '\n'; if (checkAllElements) { whole += `for (let i = 0, len = ${varName}.length; i < len; ++i) {\n`; } whole += indentString(`\ await page.evaluate((e, i) => { ${indentString(generateCheckObjectPaths(), 1)} const nonMatchingProps = []; const ${varDict} = [${props.join(',')}]; const undefProps = [${undefProps.join(',')}]; for (const prop of undefProps) { checkObjectPaths(e, prop, val => { if (val !== undefined && val !== null) { ${indentString(unexpectedPropError, 4)} return; }${indentString(expectedPropError, 3)} }, prop => {${indentString(expectedPropError, 3)} }); } for (const [${varKey}, ${varValue}] of ${varDict}) { checkObjectPaths(e, ${varKey}, val => { if (val === undefined || val === null) {${indentString(unknown, 4)} return; } const prop = String(val); ${indentString(checks.join('\n'), 3)} }, ${varKey} => {${indentString(unknown, 3)} }); } if (nonMatchingProps.length !== 0) { const props = nonMatchingProps.join("; "); let err = "The following errors happened (for ${xpath}\`${selector.value}\`): [" + props \ + "]"; if (i !== undefined) { err += ' (on the element number ' + i + ')'; } throw err; } `, indent); if (checkAllElements) { whole += ` }, ${varName}[i], i); }`; } else { whole += `}, ${varName}, undefined);`; } return { 'instructions': [whole], 'wait': false, 'warnings': warnings, 'checkResult': true, }; } // Possible inputs: // // * ("CSS", {"DOM property": "value"}) // * ("XPath", {"DOM property": "value"}) // * ("CSS", {"DOM property": "value"}, IDENT) // * ("XPath", {"DOM property": "value"}, IDENT) // * ("CSS", {"DOM property": "value"}, [IDENT]) // * ("XPath", {"DOM property": "value"}, [IDENT}) function parseAssertProperty(parser) { return parseAssertPropertyInner(parser, false); } // Possible inputs: // // * ("CSS", {"DOM property": "value"}) // * ("XPath", {"DOM property": "value"}) // * ("CSS", {"DOM property": "value"}, IDENT) // * ("XPath", {"DOM property": "value"}, IDENT) // * ("CSS", {"DOM property": "value"}, [IDENT]) // * ("XPath", {"DOM property": "value"}, [IDENT}) function parseAssertPropertyFalse(parser) { return parseAssertPropertyInner(parser, true); } function parseAssertAttributeInner(parser, assertFalse) { const identifiers = ['CONTAINS', 'ENDS_WITH', 'STARTS_WITH', 'NEAR', 'ALL']; const jsonValidator = { kind: 'json', keyTypes: { 'string': [], }, valueTypes: { 'string': {}, 'number': { allowNegative: true, allowFloat: true, }, 'ident': { allowed: ['null'], }, }, }; const ret = validator(parser, { kind: 'tuple', elements: [ { kind: 'selector' }, jsonValidator, { kind: 'ident', allowed: identifiers, optional: true, alternatives: [ { kind: 'array', valueTypes: { 'ident': { allowed: identifiers, }, }, }, ], }, ], }, ); if (hasError(ret)) { return ret; } const tuple = ret.value.entries; const warnings = []; const enabledChecks = new Set(); if (tuple.length > 2) { fillEnabledChecksV2(tuple[2], enabledChecks, warnings, 'third'); } const selector = tuple[0].value; const xpath = selector.isXPath ? 'XPath ' : 'selector '; const varName = 'parseAssertElemAttr'; const varDict = varName + 'Dict'; const varKey = varName + 'Attribute'; const varValue = varName + 'Value'; const { checks, hasSpecialChecks } = makeExtendedChecks( enabledChecks, assertFalse, 'nonMatchingAttrs', 'attribute', 'attr', varKey, varValue); const json = tuple[1].value.entries; const isPseudo = !selector.isXPath && selector.pseudo !== null; if (isPseudo) { warnings.push(`Pseudo-elements (\`${selector.pseudo}\`) don't have attributes so \ the check will be performed on the element itself`); } // JSON.stringify produces a problematic output so instead we use this. const tests = []; const nullAttributes = []; for (const [k, v] of json) { if (v.kind !== 'ident') { tests.push(`"${k}":"${v.value}"`); } else { nullAttributes.push(`"${k}"`); } } if (nullAttributes.length > 0 && hasSpecialChecks) { const k = [...enabledChecks].filter(k => k !== 'ALL').join(', '); warnings.push(`Special checks (${k}) will be ignored for \`null\``); } let noAttrError = ''; let unexpectedAttrError = ''; let expectedAttrError = ''; if (!assertFalse) { noAttrError = ` nonMatchingAttrs.push("No attribute named \`" + ${varKey} + "\`");`; unexpectedAttrError = ` nonMatchingAttrs.push("Expected attribute \`" + attr + "\` to not exist, found: \`" + \ e.getAttribute(attr) + "\`");`; } else { expectedAttrError = ` nonMatchingAttrs.push("Attribute named \`" + attr + "\` doesn't exist");`; } const code = `const nonMatchingAttrs = []; const ${varDict} = {${tests.join(',')}}; const nullAttributes = [${nullAttributes.join(',')}]; for (const attr of nullAttributes) { if (e.hasAttribute(attr)) {${unexpectedAttrError} continue; }${expectedAttrError} } for (const [${varKey}, ${varValue}] of Object.entries(${varDict})) { if (!e.hasAttribute(${varKey})) {${noAttrError} continue; } const attr = e.getAttribute(${varKey}); ${indentString(checks.join('\n'), 1)} } if (nonMatchingAttrs.length !== 0) { const props = nonMatchingAttrs.join("; "); throw "The following errors happened (for ${xpath}\`${selector.value}\`): [" + props + "]"; }`; let instructions; if (enabledChecks.has('ALL')) { instructions = `\ ${getAndSetElements(selector, varName, true)} for (let i = 0, len = ${varName}.length; i < len; ++i) { await page.evaluate(e => { ${indentString(code, 2)} }, ${varName}[i]); }`; } else { instructions = `\ ${getAndSetElements(selector, varName, false)} await page.evaluate(e => { ${indentString(code, 1)} }, ${varName});`; } return { 'instructions': [instructions], 'wait': false, 'warnings': warnings, 'checkResult': true, }; } // Possible inputs: // // * ("CSS", {"attribute name": "value"}) // * ("XPath", {"attribute name": "value"}) // * ("CSS", {"attribute name": "value"}, IDENT) // * ("XPath", {"attribute name": "value"}, IDENT) // * ("CSS", {"attribute name": "value"}, [IDENT]) // * ("XPath", {"attribute name": "value"}, [IDENT]) function parseAssertAttribute(parser) { return parseAssertAttributeInner(parser, false); } // Possible inputs: // // * ("CSS", {"attribute name": "value"}) // * ("XPath", {"attribute name": "value"}) // * ("CSS", {"attribute name": "value"}, IDENT) // * ("XPath", {"attribute name": "value"}, IDENT) // * ("CSS", {"attribute name": "value"}, [IDENT]) // * ("XPath", {"attribute name": "value"}, [IDENT]) function parseAssertAttributeFalse(parser) { return parseAssertAttributeInner(parser, true); } function parseAssertCountInner(parser, assertFalse) { const ret = validator(parser, { kind: 'tuple', elements: [ { kind: 'selector' }, { kind: 'number', allowFloat: false, allowNegative: false, }, ], }, ); if (hasError(ret)) { return ret; } const tuple = ret.value.entries; const selector = tuple[0].value; const occurences = tuple[1].value.getRaw(); const [insertBefore, insertAfter] = getInsertStrings(assertFalse, false); const varName = 'parseAssertElemCount'; const selectorS = selector.isXPath ? `::-p-xpath(${selector.value})` : selector.value; // TODO: maybe check differently depending on the tag kind? const code = `\ let ${varName} = await page.$$("${selectorS}"); ${varName} = ${varName}.length; ${insertBefore}if (${varName} !== ${occurences}) { throw 'expected ${occurences} elements, found ' + ${varName}; }${insertAfter}`; return { 'instructions': [code], 'wait': false, 'checkResult': true, }; } // Possible inputs: // // * ("CSS", number of occurences [integer]) // * ("XPath", number of occurences [integer]) function parseAssertCount(parser) { return parseAssertCountInner(parser, false); } // Possible inputs: // // * ("CSS", number of occurences [integer]) // * ("XPath", number of occurences [integer]) function parseAssertCountFalse(parser) { return parseAssertCountInner(parser, true); } function parseAssertTextInner(parser, assertFalse) { const identifiers = ['CONTAINS', 'ENDS_WITH', 'STARTS_WITH', 'ALL']; const ret = validator(parser, { kind: 'tuple', elements: [ { kind: 'selector' }, { kind: 'string' }, { kind: 'ident', allowed: identifiers, optional: true, alternatives: [ { kind: 'array', valueTypes: { 'ident': { allowed: identifiers, }, }, }, ], }, ], }, ); if (hasError(ret)) { return ret; } const tuple = ret.value.entries; const warnings = []; const enabledChecks = new Set(); if (tuple.length > 2) { fillEnabledChecksV2(tuple[2], enabledChecks, warnings, 'third'); } const selector = tuple[0].value; const isPseudo = !selector.isXPath && selector.pseudo !== null; if (isPseudo) { warnings.push(`Pseudo-elements (\`${selector.pseudo}\`) don't have text so \ the check will be performed on the element itself`); } const varName = 'parseAssertElemStr'; const checks = makeTextExtendedChecks(enabledChecks, assertFalse); let checker; if (enabledChecks.has('ALL')) { checker = `\ for (const elem of ${varName}) { await checkTextForElem(elem); }`; } else { checker = `await checkTextForElem(${varName});`; } const instructions = `\ async function checkTextForElem(elem) { await elem.evaluate(e => { const errors = []; const value = ${tuple[1].value.displayInCode()}; const elemText = browserUiTestHelpers.getElemText(e, value); ${indentString(checks.join('\n'), 2)} if (errors.length !== 0) { const errs = errors.join("; "); throw "The following errors happened: [" + errs + "]"; } }); } ${getAndSetElements(selector, varName, enabledChecks.has('ALL'))} ${checker}`; return { 'instructions': [instructions], 'wait': false, 'checkResult': true, 'warnings': warnings, }; } // Possible inputs: // // * ("CSS selector", text [STRING]) // * ("XPath", text [STRING]) // * ("CSS selector", text [STRING], ALL) // * ("XPath", text [STRING], ALL) function parseAssertText(parser) { return parseAssertTextInner(parser, false); } // Possible inputs: // // * ("CSS selector", text [STRING]) // * ("XPath", text [STRING]) // * ("CSS selector", text [STRING], ALL) // * ("XPath", text [STRING], ALL) function parseAssertTextFalse(parser) { return parseAssertTextInner(parser, true); } function parseAssertInner(parser, assertFalse) { const ret = validator(parser, { kind: 'tuple', elements: [ { kind: 'boolean', alternatives: [ { kind: 'selector' }, ], }, ], alternatives: [ { kind: 'boolean' }, { kind: 'selector' }, ], }, ); if (hasError(ret)) { return ret; } let value = ret.value; if (ret.kind === 'tuple') { value = value.entries[0].value; } const [insertBefore, insertAfter] = getInsertStrings(assertFalse, false); if (value.kind === 'boolean') { let extra = ''; if (value.originallyExpression) { extra = ` (\`${cleanString(value.getRaw())}\`)`; } return { 'instructions': [`\ function compareArrayLike(t1, t2) { if (t1.length !== t2.length) { return false; } for (const [index, value] of t1.entries()) { if (value !== t2[index]) { return false; } } return true; } function compareJson(j1, j2) { for (const key of Object.keys(j1)) { if (j2[key] !== j1[key]) { return false; } } for (const key of Object.keys(j2)) { if (j2[key] !== j1[key]) { return false; } } return true; } const check = ${value.value}; ${insertBefore}if (!check) { throw "Condition \`${cleanString(value.getErrorText())}\`${extra} was evaluated as false"; }${insertAfter}`], 'wait': false, 'checkResult': true, }; } const selectorS = value.isXPath ? `::-p-xpath(${value.value})` : value.value; return { 'instructions': [ `\ ${insertBefore}if ((await page.$("${selectorS}")) === null) { throw '"${value.value}" not found'; }${insertAfter}`, ], 'wait': false, 'checkResult': true, }; } // Possible inputs: // // * "CSS selector" // * "XPath" // * boolean // * ("CSS selector") // * ("XPath") // * (boolean) function parseAssert(parser) { return parseAssertInner(parser, false); } // Possible inputs: // // * "CSS selector" // * "XPath" // * boolean // * ("CSS selector") // * ("XPath") // * (boolean) function parseAssertFalse(parser) { return parseAssertInner(parser, true); } function parseAssertPositionInner(parser, assertFalse) { const jsonValidator = { kind: 'json', keyTypes: { string: ['x', 'y'], }, valueTypes: { number: { allowNegative: true, allowFloat: true, }, }, }; const ret = validator(parser, { kind: 'tuple', elements: [ { kind: 'selector' }, jsonValidator, { kind: 'ident', allowed: ['ALL'], optional: true, }, ], }, ); if (hasError(ret)) { return ret; } const tuple = ret.value.entries; const warnings = []; const enabledChecks = new Set(); if (tuple.length > 2) { fillEnabledChecksV2(tuple[2], enabledChecks, warnings, 'third'); } const selector = tuple[0].value; const checks = validatePositionDictV2(tuple[1].value.entries); const errorsVarName = 'errors'; const varName = 'assertPosition'; const whole = `\ ${getAndSetElements(selector, varName, enabledChecks.has('ALL'))} ${commonPositionCheckCode( selector, checks, enabledChecks.has('ALL'), varName, errorsVarName, assertFalse, )} if (${errorsVarName}.length > 0) { throw "The following errors happened: [" + ${errorsVarName}.join("; ") + "]"; }`; return { 'instructions': [whole], 'warnings': warnings, 'wait': false, 'checkResult': true, }; } // Possible inputs: // // * ("CSS selector" | "XPath", {"X" | "Y": integer}) // * ("CSS selector" | "XPath", {"X" | "Y": integer}, ALL) function parseAssertPosition(parser) { return parseAssertPositionInner(parser, false); } // Possible inputs: // // * ("CSS selector" | "XPath", {"X" | "Y": integer}) // * ("CSS selector" | "XPath", {"X" | "Y": integer}, ALL) function parseAssertPositionFalse(parser) { return parseAssertPositionInner(parser, true); } function parseAssertLocalStorageInner(parser, assertFalse) { const ret = validator(parser, { kind: 'json', keyTypes: { 'string': [], }, valueTypes: { 'string': {}, 'number': { allowNegative: true, allowFloat: true, }, 'ident': { allowed: ['null'], }, }, }, ); if (hasError(ret)) { return ret; } const json = ret.value.entries; let d = ''; for (const [key, value] of json) { let value_s; if (value.kind === 'ident') { value_s = value.value; } else { value_s = `"${value.value}"`; } if (d.length > 0) { d += ','; } d += `"${key}":${value_s}`; } if (d.length === 0) { return { 'instructions': [], 'warnings': [], 'wait': false, }; } const varName = 'localStorageElem'; const varDict = `${varName}Dict`; const varKey = `${varName}Key`; const varValue = `${varName}Value`; const checkSign = assertFalse ? '==' : '!='; const code = `\ await page.evaluate(() => { const errors = []; const ${varDict} = {${d}}; for (const [${varKey}, ${varValue}] of Object.entries(${varDict})) { let ${varName} = window.localStorage.getItem(${varKey}); if (${varName} ${checkSign} ${varValue}) { errors.push("localStorage item \\"" + ${varKey} + "\\" (of value \\"" + ${varValue} + \ "\\") ${checkSign} \\"" + ${varName} + "\\""); } } if (errors.length !== 0) { const errs = errors.join("; "); throw "The following errors happened: [" + errs + "]"; } });`; return { 'instructions': [code], 'warnings': [], 'wait': false, 'checkResult': true, }; } // Possible inputs: // // * JSON object (for example: {"key": "expected value", "another key": "another expected"}) function parseAssertLocalStorage(parser) { return parseAssertLocalStorageInner(parser, false); } // Possible inputs: // // * JSON object (for example: {"key": "unexpected value", "another key": "another unexpected"}) function parseAssertLocalStorageFalse(parser) { return parseAssertLocalStorageInner(parser, true); } function parseAssertVariableInner(parser, assertFalse) { const identifiers = ['CONTAINS', 'STARTS_WITH', 'ENDS_WITH', 'NEAR']; const ret = validator(parser, { kind: 'tuple', elements: [ { kind: 'ident' }, { kind: 'json', keyTypes: { 'string': [], }, valueTypes: { 'string': {}, 'number': { allowNegative: true, allowFloat: true, }, 'ident': {}, }, alternatives: [ { kind: 'string' }, { kind: 'number', allowFloat: true, allowNegative: true, }, ], }, { kind: 'ident', allowed: identifiers, optional: true, alternatives: [ { kind: 'array', valueTypes: { 'ident': { allowed: identifiers, }, }, }, ], }, ], }, ); if (hasError(ret)) { return ret; } const tuple = ret.value.entries; const warnings = []; const enabledChecks = new Set(); if (tuple.length > 2) { fillEnabledChecksV2(tuple[2], enabledChecks, warnings, 'third'); } const checks = []; if (enabledChecks.has('CONTAINS')) { if (assertFalse) { checks.push(`\ if (value1.indexOf(value2) !== -1) { errors.push("\`" + value1 + "\` contains \`" + value2 + "\` (for CONTAINS check)"); }`); } else { checks.push(`\ if (value1.indexOf(value2) === -1) { errors.push("\`" + value1 + "\` doesn't contain \`" + value2 + "\` (for CONTAINS check)"); }`); } } if (enabledChecks.has('STARTS_WITH')) { if (assertFalse) { checks.push(`\ if (value1.startsWith(value2)) { errors.push("\`" + value1 + "\` starts with \`" + value2 + "\` (for STARTS_WITH check)"); }`); } else { checks.push(`\ if (!value1.startsWith(value2)) { errors.push("\`" + value1 + "\` doesn't start with \`" + value2 + "\` (for STARTS_WITH check)"); }`); } } if (enabledChecks.has('ENDS_WITH')) { if (assertFalse) { checks.push(`\ if (value1.endsWith(value2)) { errors.push("\`" + value1 + "\` ends with \`" + value2 + "\` (for ENDS_WITH check)"); }`); } else { checks.push(`\ if (!value1.endsWith(value2)) { errors.push("\`" + value1 + "\` doesn't end with \`" + value2 + "\` (for ENDS_WITH check)"); }`); } } if (enabledChecks.has('NEAR')) { if (assertFalse) { checks.push(`\ const parsedInt = Number.parseInt(value1, 10); if (Number.isNaN(parsedInt)) { errors.push('\`' + value1 + '\` is not a number (for NEAR check)'); } else if (Math.abs(parsedInt - value2) <= 1) { errors.push('\`' + value1 + '\` is within 1 of \`' + value2 + '\` (for NEAR check)'); }`); } else { checks.push(`\ const parsedInt = Number.parseInt(value1, 10); if (Number.isNaN(parsedInt)) { errors.push('\`' + value1 + '\` is not a number (for NEAR check)'); } else if (Math.abs(parsedInt - value2) > 1) { errors.push('\`' + value1 + '\` is not within 1 of \`' + value2 + '\` (for NEAR \ check)'); }`); } } if (checks.length === 0) { if (assertFalse) { checks.push(`\ if (value1 === value2) { errors.push("\`" + value1 + "\` is equal to \`" + value2 + "\`"); }`); } else { checks.push(`\ if (value1 !== value2) { errors.push("\`" + value1 + "\` isn't equal to \`" + value2 + "\`"); }`); } } return { 'instructions': [`\ function stringifyValue(value) { if (['number', 'string', 'boolean'].indexOf(typeof value) !== -1) { return String(value); } return JSON.stringify(value); } const value1 = stringifyValue(arg.variables.get("${tuple[0].value.displayInCode()}")); const value2 = stringifyValue(${tuple[1].value.displayInCode()}); const errors = []; ${checks.join('\n')} if (errors.length !== 0) { const errs = errors.join("; "); throw "The following errors happened: [" + errs + "]"; }`, ], 'wait': false, 'warnings': warnings, 'checkResult': true, }; } // Possible inputs: // // * (ident, "string" | number | JSON dict) function parseAssertVariable(parser) { return parseAssertVariableInner(parser, false); } // Possible inputs: // // * (ident, "string" | number | JSON dict) function parseAssertVariableFalse(parser) { return parseAssertVariableInner(parser, true); } function parseAssertSizeInner(parser, assertFalse) { const jsonValidator = { kind: 'json', keyTypes: { 'string': ['height', 'width'], }, valueTypes: { 'number': { allowNegative: true, allowFloat: true, }, }, }; const ret = validator(parser, { kind: 'tuple', elements: [ { kind: 'selector' }, jsonValidator, { kind: 'ident', allowed: ['ALL'], optional: true, }, ], }, ); if (hasError(ret)) { return ret; } const tuple = ret.value.entries; const checkAllElements = tuple.length > 2; const json = tuple[1].value.entries; const selector = tuple[0].value; const varName = 'assertSizeElem'; const errorVarName = 'errors'; const errorPosVarName = 'erroredElem'; const instructions = `\ ${getAndSetElements(selector, varName, checkAllElements)} let ${errorPosVarName} = null; ${commonSizeCheckCode( selector, checkAllElements, assertFalse, json, varName, errorVarName, errorPosVarName)} if (${errorVarName}.length !== 0) { const errs = ${errorVarName}.join("; "); let err = "The following errors happened: [" + errs + "]"; if (${errorPosVarName} !== null) { err += ' (on the element number ' + ${errorPosVarName} + ')'; } throw err; } `; return { 'instructions': [instructions], 'wait': false, 'checkResult': true, }; } // Possible inputs: // // * ("CSS selector" | "XPath", {"width"|"height": number}) function parseAssertSize(parser) { return parseAssertSizeInner(parser, false); } // Possible inputs: // // * ("CSS selector" | "XPath", {"width"|"height": number}) function parseAssertSizeFalse(parser) { return parseAssertSizeInner(parser, true); } function parseAssertClipboardInner(parser, assertFalse) { const identifiers = ['CONTAINS', 'ENDS_WITH', 'STARTS_WITH']; const ret = validator(parser, { kind: 'string', alternatives: [ { kind: 'tuple', elements: [ { kind: 'string' }, { kind: 'ident', allowed: identifiers, optional: true, alternatives: [ { kind: 'array', valueTypes: { 'ident': { allowed: identifiers, }, }, }, ], }, ], }, ], }); if (hasError(ret)) { return ret; } const enabledChecks = new Set(); let value; const warnings = []; if (ret.value.kind === 'tuple') { const tuple = ret.value.entries; if (tuple.length > 1) { fillEnabledChecksV2(tuple[1], enabledChecks, warnings, 'second'); } value = tuple[0].value.displayInCode(); } else { value = ret.value.displayInCode(); } const [init, getter, callback] = checkClipboardPermission('elemText'); const command = `${init} const value = ${value}; ${getter} const errors = []; ${makeTextExtendedChecks(enabledChecks, assertFalse).join('\n')} if (errors.length !== 0) { const errs = errors.join("; "); throw "The following errors happened: [" + errs + "]"; }`; return { 'instructions': [command], 'wait': false, 'checkResult': true, 'warnings': warnings, 'afterCallback': callback, }; } // Possible inputs: // // * "string" // * ("string") // * ("string", IDENT) // * ("string", [IDENT]) function parseAssertClipboard(parser) { return parseAssertClipboardInner(parser, false); } // Possible inputs: // // * "string" // * ("string") // * ("string", IDENT) // * ("string", [IDENT]) function parseAssertClipboardFalse(parser) { return parseAssertClipboardInner(parser, true); } function parseAssertFindTextInner(parser, assertFalse) { const ret = validator(parser, { kind: 'tuple', elements: [ { kind: 'string', allowEmpty: false, }, { kind: 'json', keyTypes: { string: ['case-sensitive', 'whole-word'], }, valueTypes: { boolean: {}, }, optional: true, }, ], alternatives: [ { kind: 'string', allowEmpty: false, }, ], }, ); if (hasError(ret)) { return ret; } let caseSensitive = false; let wholeWord = false; const backward = false; const wrapAround = true; const searchInFrames = true; const showDialog = false; let needle; if (ret.value.kind !== 'tuple') { needle = ret.value.displayInCode(); } else { const tuple = ret.value.entries; needle = tuple[0].value.displayInCode(); if (tuple.length > 1) { const extraValues = tuple[1].value.entries; if (extraValues.has('case-sensitive')) { caseSensitive = extraValues.get('case-sensitive').value; } if (extraValues.has('whole-word')) { wholeWord = extraValues.get('whole-word').value; } } } let comparison; if (assertFalse) { comparison = `\ if (found) { throw new Error("Found text \`" + ${needle} + "\`"); }`; } else { comparison = `\ if (!found) { throw new Error("Didn't find text \`" + ${needle} + "\`"); }`; } const code = `\ await page.evaluate(() => { const found = window.find( ${needle}, ${caseSensitive}, ${backward}, ${wrapAround}, ${wholeWord}, ${searchInFrames}, ${showDialog}, ); ${indentString(comparison, 1)} });`; return { 'instructions': [code], 'wait': false, 'checkResult': true, }; } // Possible inputs: // // * ("string") // * ("string", JSON object) function parseAssertFindText(parser) { return parseAssertFindTextInner(parser, false); } // Possible inputs: // // * ("string") // * ("string", JSON object) function parseAssertFindTextFalse(parser) { return parseAssertFindTextInner(parser, true); } module.exports = { 'parseAssert': parseAssert, 'parseAssertFalse': parseAssertFalse, 'parseAssertAttribute': parseAssertAttribute, 'parseAssertAttributeFalse': parseAssertAttributeFalse, 'parseAssertClipboard': parseAssertClipboard, 'parseAssertClipboardFalse': parseAssertClipboardFalse, 'parseAssertCount': parseAssertCount, 'parseAssertCountFalse': parseAssertCountFalse, 'parseAssertCss': parseAssertCss, 'parseAssertCssFalse': parseAssertCssFalse, 'parseAssertDocumentProperty': parseAssertDocumentProperty, 'parseAssertDocumentPropertyFalse': parseAssertDocumentPropertyFalse, 'parseAssertFindText': parseAssertFindText, 'parseAssertFindTextFalse': parseAssertFindTextFalse, 'parseAssertLocalStorage': parseAssertLocalStorage, 'parseAssertLocalStorageFalse': parseAssertLocalStorageFalse, 'parseAssertPosition': parseAssertPosition, 'parseAssertPositionFalse': parseAssertPositionFalse, 'parseAssertProperty': parseAssertProperty, 'parseAssertPropertyFalse': parseAssertPropertyFalse, 'parseAssertSize': parseAssertSize, 'parseAssertSizeFalse': parseAssertSizeFalse, 'parseAssertText': parseAssertText, 'parseAssertTextFalse': parseAssertTextFalse, 'parseAssertVariable': parseAssertVariable, 'parseAssertVariableFalse': parseAssertVariableFalse, 'parseAssertWindowProperty': parseAssertWindowProperty, 'parseAssertWindowPropertyFalse': parseAssertWindowPropertyFalse, };