feat(selectors): support regular expressions in attribute selectors (#12960)

Supports inline regex in addition to string: `_react=BookItem[author = /Ann?a/i]`.
This is similar to `text=` selector, but applies to `_react` and `_vue`
selectors. In the future, will also apply to `role=` selector.
This commit is contained in:
Dmitry Gozman 2022-03-22 17:00:56 -07:00 committed by GitHub
parent 541fb39a51
commit 722302799e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 82 additions and 5 deletions

View file

@ -765,7 +765,7 @@ Selector examples:
- match by component and property value **prefix**: `_react=BookItem[author ^= "Steven"]`
- match by component and property value **suffix**: `_react=BookItem[author $= "Steven"]`
- match by component and **key**: `_react=BookItem[key = '2']`
- match by property value **regex**: `_react=[author = /Steven(\\s+King)?/i]`
To find React element names in a tree use [React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi).
@ -801,6 +801,7 @@ Selector examples:
- match by **nested** property value: `_vue=[some.nested.value = 12]`
- match by component and property value **prefix**: `_vue=book-item[author ^= "Steven"]`
- match by component and property value **suffix**: `_vue=book-item[author $= "Steven"]`
- match by property value **regex**: `_vue=[author = /Steven(\\s+King)?/i]`
To find Vue element names in a tree use [Vue DevTools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=en).

View file

@ -32,13 +32,20 @@ export function checkComponentAttribute(obj: any, attr: ParsedComponentAttribute
if (obj !== undefined && obj !== null)
obj = obj[token];
}
const objValue = typeof obj === 'string' && !attr.caseSensitive ? obj.toUpperCase() : obj;
return matchesAttribute(obj, attr);
}
export function matchesAttribute(value: any, attr: ParsedComponentAttribute) {
const objValue = typeof value === 'string' && !attr.caseSensitive ? value.toUpperCase() : value;
const attrValue = typeof attr.value === 'string' && !attr.caseSensitive ? attr.value.toUpperCase() : attr.value;
if (attr.op === '<truthy>')
return !!objValue;
if (attr.op === '=')
if (attr.op === '=') {
if (attrValue instanceof RegExp)
return typeof objValue === 'string' && !!objValue.match(attrValue);
return objValue === attrValue;
}
if (typeof objValue !== 'string' || typeof attrValue !== 'string')
return false;
if (attr.op === '*=')
@ -100,6 +107,39 @@ export function parseComponentSelector(selector: string): ParsedComponentSelecto
return result;
}
function readRegularExpression() {
if (eat1() !== '/')
syntaxError('parsing regular expression');
let source = '';
let inClass = false;
// https://262.ecma-international.org/11.0/#sec-literals-regular-expression-literals
while (!EOL) {
if (next() === '\\') {
source += eat1();
if (EOL)
syntaxError('parsing regular expressiion');
} else if (inClass && next() === ']') {
inClass = false;
} else if (!inClass && next() === '[') {
inClass = true;
} else if (!inClass && next() === '/') {
break;
}
source += eat1();
}
if (eat1() !== '/')
syntaxError('parsing regular expression');
let flags = '';
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
while (!EOL && next().match(/[dgimsuy]/))
flags += eat1();
try {
return new RegExp(source, flags);
} catch (e) {
throw new Error(`Error while parsing selector \`${selector}\`: ${e.message}`);
}
}
function readAttributeToken() {
let token = '';
skipSpaces();
@ -150,7 +190,11 @@ export function parseComponentSelector(selector: string): ParsedComponentSelecto
let value = undefined;
let caseSensitive = true;
skipSpaces();
if (next() === `'` || next() === `"`) {
if (next() === '/') {
if (operator !== '=')
throw new Error(`Error while parsing selector \`${selector}\` - cannot use ${operator} in attribute with regular expression`);
value = readRegularExpression();
} else if (next() === `'` || next() === `"`) {
value = readQuotedString(next()).slice(1, -1);
skipSpaces();
if (next() === 'i' || next() === 'I') {

View file

@ -23,7 +23,8 @@ const serialize = (parsed: ParsedComponentSelector) => {
const path = attr.jsonPath.map(token => /^[a-zA-Z0-9]+$/i.test(token) ? token : JSON.stringify(token)).join('.');
if (attr.op === '<truthy>')
return '[' + path + ']';
return '[' + path + ' ' + attr.op + ' ' + JSON.stringify(attr.value) + (attr.caseSensitive ? ']' : ' i]');
const value = attr.value instanceof RegExp ? attr.value.toString() : JSON.stringify(attr.value);
return '[' + path + ' ' + attr.op + ' ' + value + (attr.caseSensitive ? ']' : ' i]');
}).join('');
};
@ -99,6 +100,13 @@ it('shoulud parse bool', async () => {
expect(serialize(parse(`ColorButton[ enabled =true][ color = "red"i][nested.index = 6]`))).toBe('ColorButton[enabled = true][color = "red" i][nested.index = 6]');
});
it('should parse regex', async () => {
expect(serialize(parse(`ColorButton[color = /red$/]`))).toBe('ColorButton[color = /red$/]');
expect(serialize(parse(`ColorButton[color=/red/ig]`))).toBe('ColorButton[color = /red/gi]');
expect(serialize(parse(`ColorButton[color= / \\/ [/]/ ]`))).toBe('ColorButton[color = / \\/ [/]/]');
expect(serialize(parse(`ColorButton[color=/[\\]/][[/]/]`))).toBe('ColorButton[color = /[\\]/][[/]/]');
});
it('should throw on malformed selector', async () => {
expectError('foo[');
expectError('foo[');
@ -129,4 +137,10 @@ it('should throw on malformed selector', async () => {
expectError('[foo=abc \s]');
expectError('[foo=abc"\s"]');
expectError('[foo="\\"]');
expectError('[foo s]');
expectError('[foo*=/bar/]');
expectError('[foo=/bar/ s]');
expectError('[foo=/bar//]');
expectError('[foo=/bar/pt]');
expectError('[foo=/[\\]/');
});

View file

@ -102,6 +102,15 @@ for (const [name, url] of Object.entries(reacts)) {
expect(await page.$$eval(`_react=BookItem[name *= " gatsby" i]`, els => els.length)).toBe(1);
});
it('should support regex', async ({ page }) => {
expect(await page.$$eval(`_react=ColorButton[color = /red/]`, els => els.length)).toBe(3);
expect(await page.$$eval(`_react=ColorButton[color = /^red$/]`, els => els.length)).toBe(3);
expect(await page.$$eval(`_react=ColorButton[color = /RED/i]`, els => els.length)).toBe(3);
expect(await page.$$eval(`_react=ColorButton[color = /[pqr]ed/]`, els => els.length)).toBe(3);
expect(await page.$$eval(`_react=ColorButton[color = /[pq]ed/]`, els => els.length)).toBe(0);
expect(await page.$$eval(`_react=BookItem[name = /gat.by/i]`, els => els.length)).toBe(1);
});
it('should support truthy querying', async ({ page }) => {
expect(await page.$$eval(`_react=ColorButton[enabled]`, els => els.length)).toBe(5);
});

View file

@ -102,6 +102,15 @@ for (const [name, url] of Object.entries(vues)) {
expect(await page.$$eval(`_vue=book-item[name *= " gatsby" i]`, els => els.length)).toBe(1);
});
it('should support regex', async ({ page }) => {
expect(await page.$$eval(`_vue=color-button[color = /red/]`, els => els.length)).toBe(3);
expect(await page.$$eval(`_vue=color-button[color = /^red$/]`, els => els.length)).toBe(3);
expect(await page.$$eval(`_vue=color-button[color = /RED/i]`, els => els.length)).toBe(3);
expect(await page.$$eval(`_vue=color-button[color = /[pqr]ed/]`, els => els.length)).toBe(3);
expect(await page.$$eval(`_vue=color-button[color = /[pq]ed/]`, els => els.length)).toBe(0);
expect(await page.$$eval(`_vue=book-item[name = /gat.by/i]`, els => els.length)).toBe(1);
});
it('should support truthy querying', async ({ page }) => {
expect(await page.$$eval(`_vue=color-button[enabled]`, els => els.length)).toBe(5);
});