Skip to content

Commit 84ce8d9

Browse files
committed
chore: remove _react selector
When querying with `_react`, suggest an alternative locator instead.
1 parent a8728d9 commit 84ce8d9

File tree

4 files changed

+195
-126
lines changed

4 files changed

+195
-126
lines changed

packages/injected/src/injectedScript.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ export class InjectedScript {
275275
const result = this.querySelectorAll(selector, root);
276276
if (strict && result.length > 1)
277277
throw this.strictModeViolationError(selector, result);
278+
this.checkDeprecatedSelectorUsage(selector, result);
278279
return result[0];
279280
}
280281

@@ -1228,28 +1229,44 @@ export class InjectedScript {
12281229
return oneLine(`<${element.nodeName.toLowerCase()}${attrText}>${trimStringWithEllipsis(text, 50)}</${element.nodeName.toLowerCase()}>`);
12291230
}
12301231

1231-
strictModeViolationError(selector: ParsedSelector, matches: Element[]): Error {
1232+
private _generateSelectors(elements: Element[]) {
12321233
this._evaluator.begin();
12331234
beginAriaCaches();
12341235
beginDOMCaches();
12351236
try {
12361237
// Firefox is slow to access DOM bindings in the utility world, making it very expensive to generate a lot of selectors.
12371238
const maxElements = this._isUtilityWorld && this._browserName === 'firefox' ? 2 : 10;
1238-
const infos = matches.slice(0, maxElements).map(m => ({
1239+
const infos = elements.slice(0, maxElements).map(m => ({
12391240
preview: this.previewNode(m),
12401241
selector: this.generateSelectorSimple(m),
12411242
}));
1242-
const lines = infos.map((info, i) => `\n ${i + 1}) ${info.preview} aka ${asLocator(this._sdkLanguage, info.selector)}`);
1243-
if (infos.length < matches.length)
1244-
lines.push('\n ...');
1245-
return this.createStacklessError(`strict mode violation: ${asLocator(this._sdkLanguage, stringifySelector(selector))} resolved to ${matches.length} elements:${lines.join('')}\n`);
1243+
return infos.map((info, i) => `${i + 1}) ${info.preview} aka ${asLocator(this._sdkLanguage, info.selector)}`);
12461244
} finally {
12471245
endDOMCaches();
12481246
endAriaCaches();
12491247
this._evaluator.end();
12501248
}
12511249
}
12521250

1251+
strictModeViolationError(selector: ParsedSelector, matches: Element[]): Error {
1252+
const lines = this._generateSelectors(matches).map(line => `\n ` + line);
1253+
if (lines.length < matches.length)
1254+
lines.push('\n ...');
1255+
return this.createStacklessError(`strict mode violation: ${asLocator(this._sdkLanguage, stringifySelector(selector))} resolved to ${matches.length} elements:${lines.join('')}\n`);
1256+
}
1257+
1258+
checkDeprecatedSelectorUsage(selector: ParsedSelector, matches: Element[]) {
1259+
if (!matches.length)
1260+
return;
1261+
const deperecated = selector.parts.find(part => part.name === '_react' || part.name === '_vue');
1262+
if (!deperecated)
1263+
return;
1264+
const lines = this._generateSelectors(matches).map(line => `\n ` + line);
1265+
if (lines.length < matches.length)
1266+
lines.push('\n ...');
1267+
throw this.createStacklessError(`"${deperecated.name}" selector is not supported: ${asLocator(this._sdkLanguage, stringifySelector(selector))} resolved to ${matches.length} element${matches.length === 1 ? '' : 's'}:${lines.join('')}\n`);
1268+
}
1269+
12531270
createStacklessError(message: string): Error {
12541271
if (this._browserName === 'firefox') {
12551272
const error = new Error('Error: ' + message);

packages/playwright-core/src/server/frames.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,7 @@ export class Frame extends SdkObject {
785785
} else if (element) {
786786
log = ` locator resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`;
787787
}
788+
injected.checkDeprecatedSelectorUsage(info.parsed, elements);
788789
return { log, element, visible, attached: !!element };
789790
}, { info: resolved.info, root: resolved.frame === this ? scope : undefined }));
790791
const { log, visible, attached } = await progress.race(result.evaluate(r => ({ log: r.log, visible: r.visible, attached: r.attached })));
@@ -1115,6 +1116,7 @@ export class Frame extends SdkObject {
11151116
} else if (element) {
11161117
log = ` locator resolved to ${injected.previewNode(element)}`;
11171118
}
1119+
injected.checkDeprecatedSelectorUsage(info.parsed, elements);
11181120
return { log, success: !!element, element };
11191121
}, { info: resolved.info, callId: progress.metadata.id }));
11201122
const { log, success } = await progress.race(result.evaluate(r => ({ log: r.log, success: r.success })));
@@ -1444,6 +1446,8 @@ export class Frame extends SdkObject {
14441446
throw injected.strictModeViolationError(info!.parsed, elements);
14451447
else if (elements.length)
14461448
log = ` locator resolved to ${injected.previewNode(elements[0])}`;
1449+
if (info)
1450+
injected.checkDeprecatedSelectorUsage(info.parsed, elements);
14471451
return { log, ...await injected.expect(elements[0], options, elements) };
14481452
}, { info, options, callId: progress.metadata.id }));
14491453

tests/page/selectors-react.spec.ts

Lines changed: 85 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,31 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { test as it, expect } from './pageTest';
18+
import { test as it, expect as baseExpect } from './pageTest';
19+
import type { Locator } from 'playwright-core';
20+
21+
const expect = baseExpect.extend({
22+
async toHaveCountError(locator: Locator, expected: number) {
23+
try {
24+
await expect(locator).toHaveCount(expected);
25+
if (!expected)
26+
return { pass: true, message: () => 'Locator has expected count of 0' };
27+
return {
28+
pass: false,
29+
message: () => `Querying locator ${locator.toString()} should throw, but it did not.`,
30+
};
31+
} catch (e) {
32+
const message = (e as Error).message;
33+
try {
34+
expect(message).toContain(`"_react" selector is not supported`);
35+
expect(message).toContain(`resolved to ${expected} element`);
36+
} catch (error) {
37+
return { pass: false, message: () => (error as Error).message };
38+
}
39+
return { pass: true, message: () => 'Error message is as expected' };
40+
}
41+
}
42+
});
1943

2044
const reacts = {
2145
'react15': '/reading-list/react15.html',
@@ -31,139 +55,138 @@ for (const [name, url] of Object.entries(reacts)) {
3155
});
3256

3357
it('should work with single-root elements @smoke', async ({ page }) => {
34-
await expect(page.locator(`_react=BookList`)).toHaveCount(1);
35-
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
36-
await expect(page.locator(`_react=BookList >> _react=BookItem`)).toHaveCount(3);
37-
await expect(page.locator(`_react=BookItem >> _react=BookList`)).toHaveCount(0);
58+
await expect(page.locator(`_react=BookList`)).toHaveCountError(1);
59+
await expect(page.locator(`_react=BookItem`)).toHaveCountError(3);
60+
await expect(page.locator(`_react=BookList >> _react=BookItem`)).toHaveCountError(3);
61+
await expect(page.locator(`_react=BookItem >> _react=BookList`)).toHaveCountError(0);
3862
});
3963

4064
it('should work with multi-root elements (fragments)', async ({ page }) => {
4165
it.skip(name === 'react15', 'React 15 does not support fragments');
42-
await expect(page.locator(`_react=App`)).toHaveCount(15);
43-
await expect(page.locator(`_react=AppHeader`)).toHaveCount(2);
44-
await expect(page.locator(`_react=NewBook`)).toHaveCount(2);
66+
await expect(page.locator(`_react=App`)).toHaveCountError(15);
67+
await expect(page.locator(`_react=AppHeader`)).toHaveCountError(2);
68+
await expect(page.locator(`_react=NewBook`)).toHaveCountError(2);
4569
});
4670

4771
it('should not crash when there is no match', async ({ page }) => {
48-
await expect(page.locator(`_react=Apps`)).toHaveCount(0);
49-
await expect(page.locator(`_react=BookLi`)).toHaveCount(0);
72+
await expect(page.locator(`_react=Apps`)).toHaveCountError(0);
73+
await expect(page.locator(`_react=BookLi`)).toHaveCountError(0);
5074
});
5175

5276
it('should compose', async ({ page }) => {
53-
await expect(page.locator(`_react=NewBook >> _react=button`)).toHaveText('new book');
54-
expect(await page.$eval(`_react=NewBook >> _react=input`, el => el.tagName)).toBe('INPUT');
55-
await expect(page.locator(`_react=BookItem >> text=Gatsby`)).toHaveText('The Great Gatsby');
77+
await expect(page.locator(`_react=NewBook >> _react=button`).locator(':scope:has-text("new book")')).toHaveCountError(1);
78+
await expect(page.locator(`_react=BookItem >> text=Gatsby`).locator(':scope:has-text("The Great Gatsby")')).toHaveCountError(1);
5679
});
5780

5881
it('should query by props combinations', async ({ page }) => {
59-
await expect(page.locator(`_react=BookItem[name="The Great Gatsby"]`)).toHaveCount(1);
60-
await expect(page.locator(`_react=BookItem[name="the great gatsby" i]`)).toHaveCount(1);
61-
await expect(page.locator(`_react=li[key="The Great Gatsby"]`)).toHaveCount(1);
62-
await expect(page.locator(`_react=ColorButton[nested.index = 0]`)).toHaveCount(1);
63-
await expect(page.locator(`_react=ColorButton[nested.nonexisting.index = 0]`)).toHaveCount(0);
64-
await expect(page.locator(`_react=ColorButton[nested.index.nonexisting = 0]`)).toHaveCount(0);
65-
await expect(page.locator(`_react=ColorButton[nested.index.nonexisting = 1]`)).toHaveCount(0);
66-
await expect(page.locator(`_react=ColorButton[nested.value = 4.1]`)).toHaveCount(1);
67-
await expect(page.locator(`_react=ColorButton[enabled = false]`)).toHaveCount(4);
68-
await expect(page.locator(`_react=ColorButton[enabled = true] `)).toHaveCount(5);
69-
await expect(page.locator(`_react=ColorButton[enabled = true][color = "red"]`)).toHaveCount(2);
70-
await expect(page.locator(`_react=ColorButton[enabled = true][color = "red"i][nested.index = 6]`)).toHaveCount(1);
82+
await expect(page.locator(`_react=BookItem[name="The Great Gatsby"]`)).toHaveCountError(1);
83+
await expect(page.locator(`_react=BookItem[name="the great gatsby" i]`)).toHaveCountError(1);
84+
await expect(page.locator(`_react=li[key="The Great Gatsby"]`)).toHaveCountError(1);
85+
await expect(page.locator(`_react=ColorButton[nested.index = 0]`)).toHaveCountError(1);
86+
await expect(page.locator(`_react=ColorButton[nested.nonexisting.index = 0]`)).toHaveCountError(0);
87+
await expect(page.locator(`_react=ColorButton[nested.index.nonexisting = 0]`)).toHaveCountError(0);
88+
await expect(page.locator(`_react=ColorButton[nested.index.nonexisting = 1]`)).toHaveCountError(0);
89+
await expect(page.locator(`_react=ColorButton[nested.value = 4.1]`)).toHaveCountError(1);
90+
await expect(page.locator(`_react=ColorButton[enabled = false]`)).toHaveCountError(4);
91+
await expect(page.locator(`_react=ColorButton[enabled = true] `)).toHaveCountError(5);
92+
await expect(page.locator(`_react=ColorButton[enabled = true][color = "red"]`)).toHaveCountError(2);
93+
await expect(page.locator(`_react=ColorButton[enabled = true][color = "red"i][nested.index = 6]`)).toHaveCountError(1);
7194
});
7295

7396
it('should exact match by props', async ({ page }) => {
74-
await expect(page.locator(`_react=BookItem[name = "The Great Gatsby"]`)).toHaveText('The Great Gatsby');
75-
await expect(page.locator(`_react=BookItem[name = "The Great Gatsby"]`)).toHaveCount(1);
97+
await expect(page.locator(`_react=BookItem[name = "The Great Gatsby"]`).locator(':scope:has-text("The Great Gatsby")')).toHaveCountError(1);
98+
await expect(page.locator(`_react=BookItem[name = "The Great Gatsby"]`)).toHaveCountError(1);
7699
// case sensitive by default
77-
await expect(page.locator(`_react=BookItem[name = "the great gatsby"]`)).toHaveCount(0);
78-
await expect(page.locator(`_react=BookItem[name = "the great gatsby" s]`)).toHaveCount(0);
79-
await expect(page.locator(`_react=BookItem[name = "the great gatsby" S]`)).toHaveCount(0);
100+
await expect(page.locator(`_react=BookItem[name = "the great gatsby"]`)).toHaveCountError(0);
101+
await expect(page.locator(`_react=BookItem[name = "the great gatsby" s]`)).toHaveCountError(0);
102+
await expect(page.locator(`_react=BookItem[name = "the great gatsby" S]`)).toHaveCountError(0);
80103
// case insensitive with flag
81-
await expect(page.locator(`_react=BookItem[name = "the great gatsby" i]`)).toHaveCount(1);
82-
await expect(page.locator(`_react=BookItem[name = "the great gatsby" I]`)).toHaveCount(1);
83-
await expect(page.locator(`_react=BookItem[name = " The Great Gatsby "]`)).toHaveCount(0);
104+
await expect(page.locator(`_react=BookItem[name = "the great gatsby" i]`)).toHaveCountError(1);
105+
await expect(page.locator(`_react=BookItem[name = "the great gatsby" I]`)).toHaveCountError(1);
106+
await expect(page.locator(`_react=BookItem[name = " The Great Gatsby "]`)).toHaveCountError(0);
84107
});
85108

86109
it('should partially match by props', async ({ page }) => {
87110
// Check partial matching
88-
await expect(page.locator(`_react=BookItem[name *= "Gatsby"]`)).toHaveText('The Great Gatsby');
89-
await expect(page.locator(`_react=BookItem[name *= "Gatsby"]`)).toHaveCount(1);
90-
await expect(page.locator(`_react=[name *= "Gatsby"]`)).toHaveCount(1);
111+
await expect(page.locator(`_react=BookItem[name *= "Gatsby"]`).locator(':scope:has-text("The Great Gatsby")')).toHaveCountError(1);
112+
await expect(page.locator(`_react=BookItem[name *= "Gatsby"]`)).toHaveCountError(1);
113+
await expect(page.locator(`_react=[name *= "Gatsby"]`)).toHaveCountError(1);
91114

92-
await expect(page.locator(`_react=BookItem[name = "Gatsby"]`)).toHaveCount(0);
115+
await expect(page.locator(`_react=BookItem[name = "Gatsby"]`)).toHaveCountError(0);
93116
});
94117

95118
it('should support all string operators', async ({ page }) => {
96-
await expect(page.locator(`_react=ColorButton[color = "red"]`)).toHaveCount(3);
97-
await expect(page.locator(`_react=ColorButton[color |= "red"]`)).toHaveCount(3);
98-
await expect(page.locator(`_react=ColorButton[color $= "ed"]`)).toHaveCount(3);
99-
await expect(page.locator(`_react=ColorButton[color ^= "gr"]`)).toHaveCount(3);
100-
await expect(page.locator(`_react=ColorButton[color ~= "e"]`)).toHaveCount(0);
101-
await expect(page.locator(`_react=BookItem[name ~= "gatsby" i]`)).toHaveCount(1);
102-
await expect(page.locator(`_react=BookItem[name *= " gatsby" i]`)).toHaveCount(1);
119+
await expect(page.locator(`_react=ColorButton[color = "red"]`)).toHaveCountError(3);
120+
await expect(page.locator(`_react=ColorButton[color |= "red"]`)).toHaveCountError(3);
121+
await expect(page.locator(`_react=ColorButton[color $= "ed"]`)).toHaveCountError(3);
122+
await expect(page.locator(`_react=ColorButton[color ^= "gr"]`)).toHaveCountError(3);
123+
await expect(page.locator(`_react=ColorButton[color ~= "e"]`)).toHaveCountError(0);
124+
await expect(page.locator(`_react=BookItem[name ~= "gatsby" i]`)).toHaveCountError(1);
125+
await expect(page.locator(`_react=BookItem[name *= " gatsby" i]`)).toHaveCountError(1);
103126
});
104127

105128
it('should support regex', async ({ page }) => {
106-
await expect(page.locator(`_react=ColorButton[color = /red/]`)).toHaveCount(3);
107-
await expect(page.locator(`_react=ColorButton[color = /^red$/]`)).toHaveCount(3);
108-
await expect(page.locator(`_react=ColorButton[color = /RED/i]`)).toHaveCount(3);
109-
await expect(page.locator(`_react=ColorButton[color = /[pqr]ed/]`)).toHaveCount(3);
110-
await expect(page.locator(`_react=ColorButton[color = /[pq]ed/]`)).toHaveCount(0);
111-
await expect(page.locator(`_react=BookItem[name = /gat.by/i]`)).toHaveCount(1);
129+
await expect(page.locator(`_react=ColorButton[color = /red/]`)).toHaveCountError(3);
130+
await expect(page.locator(`_react=ColorButton[color = /^red$/]`)).toHaveCountError(3);
131+
await expect(page.locator(`_react=ColorButton[color = /RED/i]`)).toHaveCountError(3);
132+
await expect(page.locator(`_react=ColorButton[color = /[pqr]ed/]`)).toHaveCountError(3);
133+
await expect(page.locator(`_react=ColorButton[color = /[pq]ed/]`)).toHaveCountError(0);
134+
await expect(page.locator(`_react=BookItem[name = /gat.by/i]`)).toHaveCountError(1);
112135
});
113136

114137
it('should support truthy querying', async ({ page }) => {
115-
await expect(page.locator(`_react=ColorButton[enabled]`)).toHaveCount(5);
138+
await expect(page.locator(`_react=ColorButton[enabled]`)).toHaveCountError(5);
116139
});
117140

118141
it('should support nested react trees', async ({ page }) => {
119-
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
142+
await expect(page.locator(`_react=BookItem`)).toHaveCountError(3);
120143
await page.evaluate(() => {
121144
// @ts-ignore
122145
mountNestedApp();
123146
});
124-
await expect(page.locator(`_react=BookItem`)).toHaveCount(6);
147+
await expect(page.locator(`_react=BookItem`)).toHaveCountError(6);
125148
});
126149

127150
it('should work with react memo', async ({ page }) => {
128151
it.skip(name === 'react15' || name === 'react16', 'Class components dont support memo');
129-
await expect(page.locator(`_react=ButtonGrid`)).toHaveCount(9);
152+
await expect(page.locator(`_react=ButtonGrid`)).toHaveCountError(9);
130153
});
131154

132155
it('should work with multiroot react', async ({ page }) => {
133156
await it.step('mount second root', async () => {
134-
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
157+
await expect(page.locator(`_react=BookItem`)).toHaveCountError(3);
135158
await page.evaluate(() => {
136159
const anotherRoot = document.createElement('div');
137160
anotherRoot.id = 'root2';
138161
document.body.append(anotherRoot);
139162
// @ts-ignore
140163
window.mountApp(anotherRoot);
141164
});
142-
await expect(page.locator(`_react=BookItem`)).toHaveCount(6);
165+
await expect(page.locator(`_react=BookItem`)).toHaveCountError(6);
143166
});
144167

145168
await it.step('add a new book to second root', async () => {
146169
await page.locator('#root2 input').fill('newbook');
147170
await page.locator('#root2 >> text=new book').click();
148-
await expect(page.locator('css=#root >> _react=BookItem')).toHaveCount(3);
149-
await expect(page.locator('css=#root2 >> _react=BookItem')).toHaveCount(4);
171+
await expect(page.locator('css=#root >> _react=BookItem')).toHaveCountError(3);
172+
await expect(page.locator('css=#root2 >> _react=BookItem')).toHaveCountError(4);
150173
});
151174
});
152175

153176
it('should work with multiroot react inside shadow DOM', async ({ page }) => {
154-
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
177+
await expect(page.locator(`_react=BookItem`)).toHaveCountError(3);
155178
await page.evaluate(() => {
156179
const anotherRoot = document.createElement('div');
157180
document.body.append(anotherRoot);
158181
const shadowRoot = anotherRoot.attachShadow({ mode: 'open' });
159182
// @ts-ignore
160183
window.mountApp(shadowRoot);
161184
});
162-
await expect(page.locator(`_react=BookItem`)).toHaveCount(6);
185+
await expect(page.locator(`_react=BookItem`)).toHaveCountError(6);
163186
});
164187

165188
it('should work with multiroot react after unmount', async ({ page }) => {
166-
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
189+
await expect(page.locator(`_react=BookItem`)).toHaveCountError(3);
167190

168191
await page.evaluate(() => {
169192
const anotherRoot = document.createElement('div');
@@ -172,7 +195,7 @@ for (const [name, url] of Object.entries(reacts)) {
172195
const newRoot = window.mountApp(anotherRoot);
173196
newRoot.unmount();
174197
});
175-
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
198+
await expect(page.locator(`_react=BookItem`)).toHaveCountError(3);
176199
});
177200
});
178201
}

0 commit comments

Comments
 (0)