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:
parent
541fb39a51
commit
722302799e
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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=/[\\]/');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue