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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions goml-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ assert-attribute: ("#id > .class", {"attribute-name": "attribute-value"}, ALL)
assert-attribute: ("//*[@id='id']/*[@class='class']", {"key1": "value1", "key2": "value2"}, ALL)
```

If you want to check that an attribute doesn't exist, you can use `null`:

```
// Checking that "attribute-name" doesn't exist.
assert-attribute: ("#id > .class", {"attribute-name": null})
```

You can use more specific checks as well by using one of the following identifiers: "ALL", "CONTAINS", "ENDS_WITH", "STARTS_WITH", or "NEAR".

```
Expand Down Expand Up @@ -235,6 +242,13 @@ assert-attribute-false: ("#id > .class", {"attribute-name": "attribute-value"},
assert-attribute-false: ("//*[@id='id']/*[@class='class']", {"key1": "value1", "key2": "value2"}, ALL)
```

If you want to check that an attribute does exist, you can use `null`:

```
// Checking that "attribute-name" does exist.
assert-attribute-false: ("#id > .class", {"attribute-name": null})
```

You can use more specific checks as well by using one of the following identifiers: "ALL", "CONTAINS", "ENDS_WITH", "STARTS_WITH", or "NEAR".

```
Expand Down
93 changes: 55 additions & 38 deletions src/commands/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function parseAssertCssInner(parser, assertFalse) {
const pseudo = !selector.isXPath && selector.pseudo !== null ? `, "${selector.pseudo}"` : '';

const json = selector.tuple[1].getRaw();
const entries = validateJson(json, ['string', 'number'], 'CSS property');
const entries = validateJson(json, {'string': [], 'number': []}, 'CSS property');

if (entries.error !== undefined) {
return entries;
Expand Down Expand Up @@ -179,14 +179,11 @@ function parseAssertObjPropertyInner(parser, assertFalse, objName) {
json_dict = elems[0].getRaw();
}

const entries = validateJson(json_dict, ['string', 'number'], 'property');
const entries = validateJson(json_dict, {'string': [], 'number': []}, 'property');
if (entries.error !== undefined) {
return entries;
} else if (entries.warnings !== undefined) {
for (const warning of entries.warnings) {
warnings.push(warning);
}
}
warnings.push(...entries.warnings);

if (Object.entries(entries.values).length === 0) {
return {
Expand All @@ -203,12 +200,9 @@ function parseAssertObjPropertyInner(parser, assertFalse, objName) {
const varKey = varName + 'Key';
const varValue = varName + 'Value';
// JSON.stringify produces a problematic output so instead we use this.
let d = '';
const values = [];
for (const [k, v] of Object.entries(entries.values)) {
if (d.length > 0) {
d += ',';
}
d += `"${k}":"${v}"`;
values.push(`"${k}":"${v.value}"`);
}

const checks = [];
Expand Down Expand Up @@ -304,7 +298,7 @@ if (String(${objName}[${varKey}]) != ${varValue}) {
const instructions = [`\
await page.evaluate(() => {
const nonMatchingProps = [];
const ${varDict} = {${d}};
const ${varDict} = {${values.join(',')}};
for (const [${varKey}, ${varValue}] of Object.entries(${varDict})) {
if (${objName}[${varKey}] === undefined) {${err}
continue;
Expand Down Expand Up @@ -494,27 +488,21 @@ if (String(e[${varKey}]) != ${varValue}) {
}

const json = tuple[1].getRaw();
const entries = validateJson(json, ['string', 'number'], 'property');
const entries = validateJson(json, {'string': [], 'number': []}, 'property');
if (entries.error !== undefined) {
return entries;
}
const isPseudo = !selector.isXPath && selector.pseudo !== null;
if (isPseudo) {
if (entries.warnings === undefined) {
entries.warnings = [];
}
entries.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.
let d = '';
const props = [];
for (const [k, v] of Object.entries(entries.values)) {
if (d.length > 0) {
d += ',';
}
d += `"${k}":"${v}"`;
props.push(`"${k}":"${v.value}"`);
}

let unknown = '';
Expand All @@ -533,7 +521,7 @@ the check will be performed on the element itself`);
whole += indentString(`\
await page.evaluate(e => {
const nonMatchingProps = [];
const ${varDict} = {${d}};
const ${varDict} = {${props.join(',')}};
for (const [${varKey}, ${varValue}] of Object.entries(${varDict})) {
if (e[${varKey}] === undefined) {${unknown}
continue;
Expand Down Expand Up @@ -678,23 +666,25 @@ if (!attr.endsWith(${varValue})) {
if (assertFalse) {
checks.push(`\
if (Number.isNaN(attr)) {
nonMatchingProps.push('attribute \`' + ${varKey} + '\` (\`' + attr + '\
nonMatchingAttrs.push('attribute \`' + ${varKey} + '\` (\`' + attr + '\
\`) is NaN (for NEAR check)');
} else if (Math.abs(attr - ${varValue}) <= 1) {
nonMatchingProps.push('attribute \`' + ${varKey} + '\` (\`' + attr + '\
nonMatchingAttrs.push('attribute \`' + ${varKey} + '\` (\`' + attr + '\
\`) is within 1 of \`' + ${varValue} + '\` (for NEAR check)');
}`);
} else {
checks.push(`\
if (Number.isNaN(attr)) {
nonMatchingProps.push('attribute \`' + ${varKey} + '\` (\`' + attr + '\
nonMatchingAttrs.push('attribute \`' + ${varKey} + '\` (\`' + attr + '\
\`) is NaN (for NEAR check)');
} else if (Math.abs(attr - ${varValue}) > 1) {
nonMatchingProps.push('attribute \`' + ${varKey} + '\` (\`' + attr + '\
nonMatchingAttrs.push('attribute \`' + ${varKey} + '\` (\`' + attr + '\
\`) is not within 1 of \`' + ${varValue} + '\` (for NEAR check)');
}`);
}
}
// eslint-disable-next-line no-extra-parens
const hasSpecialChecks = (enabled_checks['ALL'] && checks.length > 1) || checks.length !== 0;
if (checks.length === 0) {
if (assertFalse) {
checks.push(`\
Expand All @@ -712,36 +702,60 @@ if (attr !== ${varValue}) {
}

const json = tuple[1].getRaw();
const entries = validateJson(json, ['string', 'number'], 'attribute');
const entries = validateJson(
json,
{'string': [], 'number': [], 'ident': ['null']},
'attribute',
);
if (entries.error !== undefined) {
return entries;
}
const isPseudo = !selector.isXPath && selector.pseudo !== null;
if (isPseudo) {
if (entries.warnings === undefined) {
entries.warnings = [];
}
entries.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.
let d = '';
const tests = [];
const nullAttributes = [];
for (const [k, v] of Object.entries(entries.values)) {
if (d.length > 0) {
d += ',';
if (v.kind !== 'ident') {
tests.push(`"${k}":"${v.value}"`);
} else {
nullAttributes.push(`"${k}"`);
}
d += `"${k}":"${v}"`;
}

if (nullAttributes.length > 0 && hasSpecialChecks) {
const k = Object.entries(enabled_checks)
.filter(([k, v]) => v && k !== 'ALL')
.map(([k, _]) => k);
entries.warnings.push(`Special checks (${k.join(', ')}) will be ignored for \`null\``);
}

let noAttrError = '';
let unexpectedAttrError = '';
let expectedAttrError = '';
if (!assertFalse) {
noAttrError = `
nonMatchingAttrs.push("No attribute named \`" + ${varKey} + "\`");`;
unexpectedAttrError = `
nonMatchingAttrs.push("Expected \`null\` for attribute \`" + attr + "\`, found: \`" + \
e.getAttribute(attr) + "\`");`;
} else {
expectedAttrError = `
nonMatchingAttrs.push("Attribute named \`" + attr + "\` doesn't exist");`;
}

const code = `const nonMatchingAttrs = [];
const ${varDict} = {${d}};
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;
Expand Down Expand Up @@ -1113,7 +1127,7 @@ function parseAssertPositionInner(parser, assertFalse) {

const json = selector.tuple[1].getRaw();

const entries = validateJson(json, ['number'], 'JSON dict key');
const entries = validateJson(json, {'number': []}, 'JSON dict key');

if (entries.error !== undefined) {
return entries;
Expand All @@ -1124,9 +1138,11 @@ function parseAssertPositionInner(parser, assertFalse) {
let checks = '';
for (const [key, value] of Object.entries(entries.values)) {
if (key === 'x') {
checks += `\ncheckAssertPosBrowser(elem, 'left', 'marginLeft', 'X', ${value}, errors);`;
checks += `
checkAssertPosBrowser(elem, 'left', 'marginLeft', 'X', ${value.value}, errors);`;
} else if (key === 'y') {
checks += `\ncheckAssertPosBrowser(elem, 'top', 'marginTop', 'Y', ${value}, errors);`;
checks += `
checkAssertPosBrowser(elem, 'top', 'marginTop', 'Y', ${value.value}, errors);`;
} else {
return {
'error': 'Only accepted keys are "x" and "y", found `' +
Expand Down Expand Up @@ -1189,6 +1205,7 @@ ${indentString(code, 1)}

return {
'instructions': [whole],
'warnings': entries.warnings,
'wait': false,
'checkResult': true,
};
Expand Down
27 changes: 14 additions & 13 deletions src/commands/compare.js
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ function parseCompareElementsPositionNearInner(parser, assertFalse) {
}

const json = tuple[2].getRaw();
const entries = validateJson(json, ['number'], 'JSON dict key');
const entries = validateJson(json, {'number': []}, 'JSON dict key');

if (entries.error !== undefined) {
return entries;
Expand All @@ -634,15 +634,16 @@ function parseCompareElementsPositionNearInner(parser, assertFalse) {
const selectors = getAndSetElements(selector1, varName + '1', false) + '\n' +
getAndSetElements(selector2, varName + '2', false) + '\n';

const warnings = [];
const warnings = entries.warnings;
let code = '';
for (const [key, value] of Object.entries(entries.values)) {
const v = value.value;
if (key === 'x') {
if (value < 0) {
if (v < 0) {
return {
'error': `Delta cannot be negative (in \`"x": ${value}\`)`,
'error': `Delta cannot be negative (in \`"x": ${v}\`)`,
};
} else if (value === '0') {
} else if (v === '0') {
warnings.push(
'Delta is 0 for "X", maybe try to use `compare-elements-position` instead?');
}
Expand All @@ -651,18 +652,18 @@ function parseCompareElementsPositionNearInner(parser, assertFalse) {
'let x1 = e1.getBoundingClientRect().left;\n' +
'let x2 = e2.getBoundingClientRect().left;\n' +
'let delta = Math.abs(x1 - x2);\n' +
`if (delta > ${value}) {\n` +
`throw "delta X values too large: " + delta + " > ${value}";\n` +
`if (delta > ${v}) {\n` +
`throw "delta X values too large: " + delta + " > ${v}";\n` +
'}\n' +
insertAfter +
'}\n' +
'checkX(elem1, elem2);\n';
} else if (key === 'y') {
if (value < 0) {
if (v < 0) {
return {
'error': `Delta cannot be negative (in \`"y": ${value}\`)`,
'error': `Delta cannot be negative (in \`"y": ${v}\`)`,
};
} else if (value === '0') {
} else if (v === '0') {
warnings.push(
'Delta is 0 for "Y", maybe try to use `compare-elements-position` instead?');
}
Expand All @@ -671,8 +672,8 @@ function parseCompareElementsPositionNearInner(parser, assertFalse) {
'let y1 = e1.getBoundingClientRect().top;\n' +
'let y2 = e2.getBoundingClientRect().top;\n' +
'let delta = Math.abs(y1 - y2);\n' +
`if (delta > ${value}) {\n` +
`throw "delta Y values too large: " + delta + " > ${value}";\n` +
`if (delta > ${v}) {\n` +
`throw "delta Y values too large: " + delta + " > ${v}";\n` +
'}\n' +
insertAfter +
'}\n' +
Expand All @@ -694,7 +695,7 @@ function parseCompareElementsPositionNearInner(parser, assertFalse) {
'instructions': [instructions],
'wait': false,
'checkResult': true,
'warnings': warnings.length > 0 ? warnings : undefined,
'warnings': warnings,
};
}

Expand Down
18 changes: 6 additions & 12 deletions src/commands/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,19 +141,16 @@ function parseObjPropertyInner(parser, objName) {
};
}

let warnings = [];
const entries = validateJson(elems[0].getRaw(), ['string', 'number'], 'property');
const entries = validateJson(elems[0].getRaw(), {'string': [], 'number': []}, 'property');
if (entries.error !== undefined) {
return entries;
} else if (entries.warnings !== undefined) {
warnings = entries.warnings;
}

if (Object.entries(entries.values).length === 0) {
return {
'instructions': [],
'wait': false,
'warnings': warnings.length > 0 ? warnings : undefined,
'warnings': entries.warnings,
'checkResult': true,
};
}
Expand All @@ -164,17 +161,14 @@ function parseObjPropertyInner(parser, objName) {
const varKey = varName + 'Key';
const varValue = varName + 'Value';
// JSON.stringify produces a problematic output so instead we use this.
let d = '';
const props = [];
for (const [k, v] of Object.entries(entries.values)) {
if (d.length > 0) {
d += ',';
}
d += `"${k}":"${v}"`;
props.push(`"${k}":"${v.value}"`);
}

const instructions = [`\
await page.evaluate(() => {
const ${varDict} = {${d}};
const ${varDict} = {${props.join(',')}};
for (const [${varKey}, ${varValue}] of Object.entries(${varDict})) {
${objName}[${varKey}] = ${varValue};
}
Expand All @@ -184,7 +178,7 @@ await page.evaluate(() => {
return {
'instructions': instructions,
'wait': false,
'warnings': warnings.length > 0 ? warnings : undefined,
'warnings': entries.warnings,
'checkResult': true,
};
}
Expand Down
5 changes: 1 addition & 4 deletions src/commands/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function parseClickWithOffset(parser) {
}

const json = tuple[1].getRaw();
const entries = validateJson(json, ['number'], 'JSON dict key', ['x', 'y']);
const entries = validateJson(json, {'number': []}, 'JSON dict key', ['x', 'y']);
if (entries.error !== undefined) {
return entries;
}
Expand All @@ -107,9 +107,6 @@ function parseClickWithOffset(parser) {
const varName = 'parseClickWithOffsetVar';
const isPseudo = !selector.isXPath && selector.pseudo !== null;
if (isPseudo) {
if (entries.warnings === undefined) {
entries.warnings = [];
}
entries.warnings.push(`Pseudo-elements (\`${selector.pseudo}\`) can't be retrieved so \
\`click\` will be performed on the element directly`);
}
Expand Down
Loading