Skip to content

Commit 171f367

Browse files
authored
feat: add support for scoped queries (#258)
1 parent a83f415 commit 171f367

File tree

13 files changed

+241
-274
lines changed

13 files changed

+241
-274
lines changed

devtools/src/content-script/contentScript.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import Bridge from 'crx-bridge';
22
import setupHighlighter from './highlighter';
33

44
import parser from '../../../src/parser';
5-
import { getQueryAdvise } from '../../../src/lib';
5+
import { getAllPossibleQueries } from '../../../src/lib';
66
import inject from './lib/inject';
77
import { setup } from '../window/testing-library';
88
import onDocReady from './lib/onDocReady';
9+
import cssPath from '../../../src/lib/cssPath';
910

1011
function init() {
1112
inject('../window/testing-library.js');
@@ -17,17 +18,22 @@ function init() {
1718
hook.highlighter = setupHighlighter({ view: window, onSelectNode });
1819

1920
function onSelectNode(node) {
20-
const { data, suggestion } = getQueryAdvise({
21+
const queries = getAllPossibleQueries({
2122
rootNode: document.body,
2223
element: node,
2324
});
2425

25-
const result = parser.parse({
26-
rootNode: document.body,
27-
query: suggestion.expression,
28-
});
26+
const suggestion = Object.values(queries).find(Boolean);
2927

30-
Bridge.sendMessage('SELECT_NODE', { result, data, suggestion }, 'devtools');
28+
Bridge.sendMessage(
29+
'SELECT_NODE',
30+
{
31+
suggestion,
32+
queries,
33+
cssPath: cssPath(node, true).toString(),
34+
},
35+
'devtools',
36+
);
3137
}
3238

3339
Bridge.onMessage('PARSE_QUERY', function ({ data }) {

devtools/src/devtools/pane.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import Result from '../../../src/components/Result';
66
import inspectedWindow from './lib/inspectedWindow';
77

88
function Panel() {
9-
const [{ result }, setResult] = useState({});
9+
const [result, setResult] = useState({});
1010

1111
useEffect(() => {
12-
Bridge.onMessage('SELECT_NODE', (result) => {
13-
setResult(result.data);
12+
Bridge.onMessage('SELECT_NODE', ({ data }) => {
13+
setResult({ elements: [data] });
1414
});
15-
}, []);
15+
}, [setResult]);
1616

1717
const dispatch = (action) => {
1818
switch (action.type) {

devtools/src/devtools/panel.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import Result from '../../../src/components/Result';
77
import MenuBar from './components/MenuBar';
88

99
function Panel() {
10-
const [{ result }, setResult] = useState({ result: {} });
10+
const [result, setResult] = useState({});
1111
const editor = useRef(null);
1212

1313
useEffect(() => {
1414
Bridge.onMessage('SELECT_NODE', ({ data }) => {
15-
setResult(data);
16-
editor.current.setValue(data.suggestion?.expression || '');
15+
setResult({ elements: [data] });
16+
editor.current.setValue(data.suggestion?.snippet || '');
1717
});
1818
}, [setResult]);
1919

@@ -28,8 +28,8 @@ function Panel() {
2828
highlight: true,
2929
},
3030
'content-script',
31-
).then((data) => {
32-
setResult(data);
31+
).then(({ result }) => {
32+
setResult(result);
3333
});
3434

3535
if (action.updateEditor !== false) {

src/components/Expandable.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import IconButton from './IconButton';
33
import Scrollable from './Scrollable';
44
import { ChevronUpIcon, ChevronDownIcon } from '@primer/octicons-react';
55

6-
function Expandable({ children, className, variant }) {
6+
function Expandable({ excerpt, children, className, variant }) {
77
const [expanded, setExpanded] = useState(false);
88

99
return (
@@ -38,7 +38,7 @@ function Expandable({ children, className, variant }) {
3838
<div>&nbsp;</div>
3939
) : (
4040
<div className="truncate mr-8 w-full flex justify-between direction">
41-
{children}
41+
{excerpt || children}
4242
</div>
4343
)}
4444

src/components/Preview.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ function Preview({ markup, variant, forwardedRef, dispatch }) {
6666
}
6767

6868
case 'SELECT_NODE': {
69-
dispatch({ type: 'SET_QUERY', query: suggestion.expression });
69+
dispatch({ type: 'SET_QUERY', query: suggestion.snippet });
7070
break;
7171
}
7272

src/components/PreviewHint.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import React from 'react';
22
import Expandable from './Expandable';
33

44
function PreviewHint({ roles, suggestion }) {
5-
const expression = suggestion?.expression ? (
6-
`> ${suggestion.expression}`
5+
const expression = suggestion?.snippet ? (
6+
suggestion.snippet
77
) : (
88
<div>
99
<span className="font-bold">accessible roles: </span>
1010
{roles.join(', ')}
1111
</div>
1212
);
1313

14+
const excerpt = suggestion?.excerpt ? `> ${suggestion.excerpt}` : expression;
15+
1416
const snapshot = suggestion?.snapshot && (
1517
<div className="snapshot">
1618
<div className="py-1">&nbsp;</div>
@@ -21,7 +23,10 @@ function PreviewHint({ roles, suggestion }) {
2123
);
2224

2325
return (
24-
<Expandable className="bg-gray-200 text-gray-800 font-mono text-xs rounded fle">
26+
<Expandable
27+
excerpt={excerpt}
28+
className="bg-gray-200 text-gray-800 font-mono text-xs rounded fle"
29+
>
2530
{expression}
2631
{snapshot}
2732
</Expandable>

src/components/Result.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ function Result({ result, dispatch }) {
1414

1515
if (
1616
!result ||
17-
!result.expression ||
1817
!Array.isArray(result.elements) ||
1918
result.elements.length === 0
2019
) {
@@ -25,23 +24,19 @@ function Result({ result, dispatch }) {
2524
);
2625
}
2726

28-
const { data, suggestion, queries } = result.elements[0];
27+
const { queries } = result.elements[0];
2928
return (
3029
<div className="flex flex-col w-full h-full overflow-hidden">
3130
<Scrollable>
3231
<div className="pb-4 border-b">
3332
<ResultSuggestion
3433
result={result}
3534
dispatch={dispatch}
36-
data={data}
37-
possibleQueries={queries}
38-
suggestion={suggestion}
35+
queries={queries}
3936
/>
4037
</div>
4138

4239
<ResultQueries
43-
data={data}
44-
suggestion={suggestion}
4540
activeMethod={result.expression?.method}
4641
dispatch={dispatch}
4742
queries={queries}

src/components/ResultQueries.js

Lines changed: 8 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from 'react';
2-
import { getFieldName } from '../lib';
32
import { messages as queryGroups } from '../constants';
43

54
function Section({ children }) {
@@ -10,52 +9,15 @@ function Heading({ children }) {
109
return <h3 className="text-xs font-bold">{children}</h3>;
1110
}
1211

13-
// The reviver parses serialized regexes back to a real regexp.
14-
// This function is required if the data comes in via message transport
15-
// think of the chrome-extension.
16-
function reviver(obj) {
17-
if (typeof obj?.$regexp === 'string') {
18-
return new RegExp(obj.$regexp, obj.$flags);
19-
}
20-
21-
return obj;
22-
}
23-
24-
// we use our own stringify method instead of the one from @testing-library/dom,
25-
// because it might have been removed for message transport.
26-
function suggestionToString({ queryMethod, queryArgs } = {}) {
27-
if (!queryMethod || !queryArgs) {
28-
return '';
29-
}
30-
31-
let [text, options] = queryArgs;
32-
33-
text = typeof text === 'string' ? `'${text}'` : reviver(text);
34-
35-
options = options
36-
? `, { ${Object.entries(options)
37-
.map(([k, v]) => `${k}: ${reviver(v)}`)
38-
.join(', ')} }`
39-
: '';
40-
41-
return `${queryMethod}(${text}${options})`;
42-
}
43-
44-
const Field = React.memo(function Field({
45-
data,
46-
method,
47-
query,
48-
dispatch,
49-
active,
50-
}) {
51-
const field = getFieldName(method);
52-
const value = data[field];
12+
const Field = React.memo(function Field({ method, query, dispatch, active }) {
13+
const arg = query?.queryArgs[0] || '';
14+
const value = arg.source || arg.$regexp || arg;
5315

5416
const handleClick = value
5517
? () => {
5618
dispatch({
5719
type: 'SET_QUERY',
58-
query: `screen.${suggestionToString(query)}`,
20+
query: query.snippet,
5921
});
6022
}
6123
: undefined;
@@ -66,24 +28,23 @@ const Field = React.memo(function Field({
6628
data-clickable={!!handleClick}
6729
onClick={handleClick}
6830
>
69-
<div className="font-light text-gray-800">{field}</div>
31+
<div className="font-light text-gray-800">{method}</div>
7032
<div className="truncate">
7133
{value || <span className="text-gray-400">n/a</span>}
7234
</div>
7335
</div>
7436
);
7537
});
7638

77-
function QueryGroup({ group, queries, heading, activeMethod, dispatch, data }) {
39+
function QueryGroup({ group, queries, heading, activeMethod, dispatch }) {
7840
return (
7941
<Section key={group.type}>
8042
<Heading>{heading}</Heading>
8143
{group.queries.map((queryMethod) => (
8244
<Field
8345
key={queryMethod}
84-
data={data}
85-
method={queryMethod}
86-
query={queries[queryMethod]}
46+
method={queryMethod.replace('getBy', '')}
47+
query={queries[queryMethod.replace('getBy', '')]}
8748
dispatch={dispatch}
8849
active={queryMethod === activeMethod}
8950
/>

src/components/ResultSuggestion.js

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,50 @@ function Code({ children }) {
88
return <span className="font-bold font-mono">{children}</span>;
99
}
1010

11-
function ResultSuggestion({ data, suggestion, result, dispatch }) {
11+
const levels = {
12+
Role: 0,
13+
LabelText: 0,
14+
PlaceholderText: 0,
15+
Text: 0,
16+
DisplayValue: 0,
17+
AltText: 1,
18+
Title: 1,
19+
TestId: 2,
20+
generic: 3,
21+
};
22+
23+
function ResultSuggestion({ queries, result, dispatch }) {
24+
const suggestion = Object.values(queries).find(Boolean);
1225
const used = result?.expression || {};
1326

14-
const usingAdvisedMethod = suggestion.method === used.method;
15-
const hasNameArg = data.name && used.args?.[1]?.includes('name');
27+
if (!suggestion) {
28+
return `Hmpf. I'm afraid I don't have any suggestions for you.`;
29+
}
30+
31+
const usingAdvisedMethod = suggestion.queryMethod === used.method;
32+
const nameOption = suggestion.queryArgs[1]?.name;
33+
const hasNameArg = nameOption && used.args?.[1]?.includes('name');
1634

17-
const color = usingAdvisedMethod ? 'bg-green-600' : colors[suggestion.level];
35+
const level = levels[suggestion.queryName] ?? 3;
36+
const color = usingAdvisedMethod ? 'bg-green-600' : colors[level];
1837

1938
const target = result?.target || {};
2039

2140
let message;
2241

23-
if (suggestion.level < used.level) {
42+
if (level < used.level) {
2443
message = (
2544
<p>
2645
You&apos;re using <Code>{used.method}</Code>, which falls under{' '}
2746
<Code>{messages[used.level].heading}</Code>. Upgrading to{' '}
28-
<Code>{suggestion.method}</Code> is recommended.
47+
<Code>{suggestion.queryMethod}</Code> is recommended.
2948
</p>
3049
);
31-
} else if (suggestion.level === 0 && suggestion.method !== used.method) {
50+
} else if (level === 0 && suggestion.queryMethod !== used.method) {
3251
message = (
3352
<p>
3453
Nice! <Code>{used.method}</Code> is a great selector! Using{' '}
35-
<Code>{suggestion.method}</Code> would still be preferable though.
54+
<Code>{suggestion.queryMethod}</Code> would still be preferable though.
3655
</p>
3756
);
3857
} else if (target.tagName === 'INPUT' && !target.getAttribute('type')) {
@@ -44,9 +63,9 @@ function ResultSuggestion({ data, suggestion, result, dispatch }) {
4463
</p>
4564
);
4665
} else if (
47-
suggestion.level === 0 &&
48-
suggestion.method === 'getByRole' &&
49-
!data.name
66+
level === 0 &&
67+
suggestion.queryMethod === 'getByRole' &&
68+
!nameOption
5069
) {
5170
message = (
5271
<p>
@@ -56,9 +75,9 @@ function ResultSuggestion({ data, suggestion, result, dispatch }) {
5675
</p>
5776
);
5877
} else if (
59-
suggestion.level === 0 &&
60-
suggestion.method === 'getByRole' &&
61-
data.name &&
78+
level === 0 &&
79+
suggestion.queryMethod === 'getByRole' &&
80+
nameOption &&
6281
!hasNameArg
6382
) {
6483
message = (
@@ -83,23 +102,23 @@ function ResultSuggestion({ data, suggestion, result, dispatch }) {
83102
<div className="space-y-4 text-sm">
84103
<div className={['text-white p-4 rounded space-y-2', color].join(' ')}>
85104
<div className="font-bold text-xs">suggested query</div>
86-
{suggestion.expression && (
105+
{suggestion.excerpt && (
87106
<div className="flex justify-between">
88107
<div
89108
className="font-mono cursor-pointer text-xs"
90109
onClick={() =>
91110
dispatch({
92111
type: 'SET_QUERY',
93-
query: suggestion.expression,
112+
query: suggestion.snippet,
94113
})
95114
}
96115
>
97-
&gt; {suggestion.expression}
116+
&gt; {suggestion.excerpt}
98117
<br />
99118
</div>
100119
<CopyButton
101120
title="copy query"
102-
text={suggestion.expression}
121+
text={suggestion.snippet}
103122
variant="white"
104123
/>
105124
</div>

0 commit comments

Comments
 (0)