Skip to content

Commit 62325ff

Browse files
authored
feat: evaluate javascript query in a sandbox (#68)
1 parent 06ba92a commit 62325ff

File tree

13 files changed

+628
-135
lines changed

13 files changed

+628
-135
lines changed

src/components/Context.js

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,17 @@
1-
import React, { useContext, useCallback, useRef, useState } from 'react';
1+
import React, { useContext, useRef, useState } from 'react';
22

33
export const AppContext = React.createContext();
44

55
function AppContextProvider(props) {
66
const jsEditorRef = useRef();
77
const htmlEditorRef = useRef();
8-
const [htmlRoot, setHtmlRoot] = useState();
98
const [parsed, setParsed] = useState({});
109

11-
const setHtmlRootRef = useCallback(
12-
(node) => {
13-
setHtmlRoot(node);
14-
},
15-
[setHtmlRoot],
16-
);
17-
1810
return (
1911
<AppContext.Provider
2012
value={{
2113
jsEditorRef,
2214
htmlEditorRef,
23-
htmlRoot,
24-
setHtmlRootRef,
2515
parsed,
2616
setParsed,
2717
}}

src/components/Embedded.js

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,6 @@ import MarkupEditor from './MarkupEditor';
1414

1515
const savedState = state.load();
1616

17-
const styles = {
18-
offscreen: {
19-
position: 'absolute',
20-
left: -300,
21-
width: 100,
22-
},
23-
};
24-
2517
const SUPPORTED_PANES = {
2618
markup: true,
2719
preview: true,
@@ -33,7 +25,7 @@ const SUPPORTED_PANES = {
3325
function Embedded() {
3426
const [html, setHtml] = useState(savedState.markup || initialValues.html);
3527
const [js, setJs] = useState(savedState.query || initialValues.js);
36-
const { setParsed, htmlRoot } = useAppContext();
28+
const { setParsed } = useAppContext();
3729

3830
const location = useLocation();
3931
const params = queryString.parse(location.search);
@@ -58,16 +50,12 @@ function Embedded() {
5850
: 'grid-cols-1';
5951

6052
useEffect(() => {
61-
if (!htmlRoot) {
62-
return;
63-
}
64-
65-
const parsed = parser.parse({ htmlRoot, js });
53+
const parsed = parser.parse({ markup: html, query: js });
6654
setParsed(parsed);
6755

6856
state.save({ markup: html, query: js });
6957
state.updateTitle(parsed.expression?.expression);
70-
}, [html, js, htmlRoot]);
58+
}, [html, js]);
7159

7260
useEffect(() => {
7361
document.body.classList.add('embedded');
@@ -78,13 +66,6 @@ function Embedded() {
7866
<div
7967
className={`h-full overflow-hidden grid grid-flow-col gap-4 p-4 bg-white shadow rounded ${columnClass}`}
8068
>
81-
{/*the markup preview must always be rendered!*/}
82-
{!panes.includes('preview') && (
83-
<div style={styles.offscreen}>
84-
<Preview html={html} />
85-
</div>
86-
)}
87-
8869
{panes.map((area) => {
8970
switch (area) {
9071
case 'preview':

src/components/Playground.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,15 @@ function Playground() {
1616
const [html, setHtml] = useState(savedState.markup || initialValues.html);
1717
const [js, setJs] = useState(savedState.query || initialValues.js);
1818

19-
const { setParsed, htmlRoot } = useAppContext();
19+
const { setParsed } = useAppContext();
2020

2121
useEffect(() => {
22-
if (!htmlRoot) {
23-
return;
24-
}
25-
26-
const parsed = parser.parse({ htmlRoot, js });
22+
const parsed = parser.parse({ markup: html, query: js });
2723
setParsed(parsed);
2824

2925
state.save({ markup: html, query: js });
3026
state.updateTitle(parsed.expression?.expression);
31-
}, [htmlRoot, html, js]);
27+
}, [html, js]);
3228

3329
return (
3430
<div className="flex flex-col h-auto md:h-full w-full">

src/components/Preview.js

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import { useAppContext } from './Context';
33
import Scrollable from './Scrollable';
44
import PreviewHint from './PreviewHint';
55
import { getQueryAdvise } from '../lib';
66

7-
function Preview({ html }) {
7+
function selectByCssPath(rootNode, cssPath) {
8+
return rootNode?.querySelector(cssPath.replace(/^body > /, ''));
9+
}
10+
11+
function Preview() {
812
// Okay, listen up. `highlighted` can be a number of things, as I wanted to
913
// keep a single variable to represent the state. This to reduce bug count
1014
// by creating out-of-sync states.
@@ -21,41 +25,49 @@ function Preview({ html }) {
2125
// Indicating that the `parsed` element can be highlighted again.
2226
const [highlighted, setHighlighted] = useState(false);
2327
const [roles, setRoles] = useState([]);
28+
const { parsed, jsEditorRef } = useAppContext();
29+
const htmlRoot = useRef();
2430

25-
const { parsed, jsEditorRef, htmlRoot, setHtmlRootRef } = useAppContext();
26-
27-
const { advise } = getQueryAdvise({
28-
root: htmlRoot ? htmlRoot.firstChild : null,
31+
const { suggestion } = getQueryAdvise({
32+
rootNode: htmlRoot.current ? htmlRoot.current.firstChild : null,
2933
element: highlighted,
3034
});
3135

36+
// TestingLibraryDom?.getSuggestedQuery(highlighted, 'get').toString() : null
37+
3238
useEffect(() => {
33-
setRoles(Object.keys(parsed.roles || {}).sort());
34-
}, [parsed.roles]);
39+
setRoles(Object.keys(parsed.accessibleRoles || {}).sort());
40+
}, [parsed.accessibleRoles]);
3541

3642
useEffect(() => {
3743
if (highlighted) {
38-
parsed.targets?.forEach((el) => el.classList.remove('highlight'));
44+
parsed.elements?.forEach((el) => {
45+
const target = selectByCssPath(htmlRoot.current, el.cssPath);
46+
target?.classList.remove('highlight');
47+
});
3948
highlighted.classList?.add('highlight');
4049
} else {
4150
highlighted?.classList?.remove('highlight');
4251

4352
if (highlighted === false) {
44-
parsed.targets?.forEach((el) => el.classList.add('highlight'));
53+
parsed.elements?.forEach((el) => {
54+
const target = selectByCssPath(htmlRoot.current, el.cssPath);
55+
target?.classList.add('highlight');
56+
});
4557
}
4658
}
4759

4860
return () => highlighted?.classList?.remove('highlight');
49-
}, [highlighted, parsed.targets]);
61+
}, [highlighted, parsed.elements]);
5062

5163
const handleClick = (event) => {
52-
if (event.target === htmlRoot) {
64+
if (event.target === htmlRoot.current) {
5365
return;
5466
}
5567

5668
event.preventDefault();
5769
const expression =
58-
advise.expression ||
70+
suggestion.expression ||
5971
'// No recommendation available.\n// Add some html attributes, or\n// use container.querySelector(…)';
6072
jsEditorRef.current.setValue(expression);
6173
};
@@ -84,15 +96,15 @@ function Preview({ html }) {
8496
<Scrollable>
8597
<div
8698
className="preview"
87-
ref={setHtmlRootRef}
8899
onClick={handleClick}
89100
onMouseMove={handleMove}
90-
dangerouslySetInnerHTML={{ __html: html }}
101+
ref={htmlRoot}
102+
dangerouslySetInnerHTML={{ __html: parsed.markup }}
91103
/>
92104
</Scrollable>
93105
</div>
94106

95-
<PreviewHint roles={roles} advise={advise} />
107+
<PreviewHint roles={roles} suggestion={suggestion} />
96108
</div>
97109
);
98110
}

src/components/PreviewHint.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React from 'react';
22
import Expandable from './Expandable';
33

4-
function PreviewHint({ roles, advise }) {
5-
const expression = advise.expression ? (
6-
`> ${advise.expression}`
4+
function PreviewHint({ roles, suggestion }) {
5+
const expression = suggestion.expression ? (
6+
`> ${suggestion.expression}`
77
) : (
88
<>
99
<span className="font-bold">accessible roles: </span>

src/components/Query.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ function Query({ onChange, initialValue }) {
1212
<QueryEditor initialValue={initialValue} onChange={onChange} />
1313
</div>
1414

15-
<QueryOutput error={parsed.error} result={parsed.text} />
15+
<QueryOutput error={parsed.error?.message} result={parsed.formatted} />
1616
</div>
1717
);
1818
}

src/components/Result.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,32 @@ import ErrorBox from './ErrorBox';
33
import ResultQueries from './ResultQueries';
44
import ResultSuggestion from './ResultSuggestion';
55
import { useAppContext } from './Context';
6-
import { getQueryAdvise } from '../lib';
76
import Scrollable from './Scrollable';
87

98
function Result() {
10-
const { parsed, htmlRoot } = useAppContext();
11-
const element = parsed.target;
9+
const { parsed } = useAppContext();
1210

1311
if (parsed.error) {
14-
return <ErrorBox caption={parsed.error} body={parsed.errorBody} />;
12+
return (
13+
<ErrorBox caption={parsed.error.message} body={parsed.error.details} />
14+
);
1515
}
1616

17-
const { data, advise } = getQueryAdvise({
18-
root: htmlRoot,
19-
element,
20-
});
17+
const { data, suggestion } = parsed.elements?.[0] || {};
18+
19+
if (!data || !suggestion) {
20+
return <div />;
21+
}
2122

2223
return (
2324
<div className="flex flex-col overflow-hidden w-full h-full">
2425
<div className="flex-none pb-4 border-b">
25-
<ResultSuggestion data={data} advise={advise} />
26+
<ResultSuggestion data={data} suggestion={suggestion} />
2627
</div>
2728

2829
<div className="flex-auto">
2930
<Scrollable>
30-
<ResultQueries data={data} advise={advise} />
31+
<ResultQueries data={data} suggestion={suggestion} />
3132
</Scrollable>
3233
</div>
3334
</div>

src/components/ResultSuggestion.js

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

11-
function ResultSuggestion({ data, advise }) {
11+
function ResultSuggestion({ data, suggestion }) {
1212
const { parsed, jsEditorRef } = useAppContext();
1313

1414
const used = parsed?.expression || {};
1515

16-
const usingAdvisedMethod = advise.method === used.method;
16+
const usingAdvisedMethod = suggestion.method === used.method;
1717
const hasNameArg = data.name && used.args?.[1]?.includes('name');
1818

19-
const color = usingAdvisedMethod ? 'bg-green-600' : colors[advise.level];
19+
const color = usingAdvisedMethod ? 'bg-green-600' : colors[suggestion.level];
2020

2121
const target = parsed.target || {};
2222

23-
let suggestion;
23+
let message;
2424

25-
if (advise.level < used.level) {
26-
suggestion = (
25+
if (suggestion.level < used.level) {
26+
message = (
2727
<p>
2828
You&apos;re using <Code>{used.method}</Code>, which falls under{' '}
2929
<Code>{messages[used.level].heading}</Code>. Upgrading to{' '}
30-
<Code>{advise.method}</Code> is recommended.
30+
<Code>{suggestion.method}</Code> is recommended.
3131
</p>
3232
);
33-
} else if (advise.level === 0 && advise.method !== used.method) {
34-
suggestion = (
33+
} else if (suggestion.level === 0 && suggestion.method !== used.method) {
34+
message = (
3535
<p>
3636
Nice! <Code>{used.method}</Code> is a great selector! Using{' '}
37-
<Code>{advise.method}</Code> would still be preferable though.
37+
<Code>{suggestion.method}</Code> would still be preferable though.
3838
</p>
3939
);
4040
} else if (target.tagName === 'INPUT' && !target.getAttribute('type')) {
41-
suggestion = (
41+
message = (
4242
<p>
4343
You can unlock <Code>getByRole</Code> by adding the{' '}
4444
<Code>type=&quot;text&quot;</Code> attribute explicitly. Accessibility
4545
will benefit from it.
4646
</p>
4747
);
4848
} else if (
49-
advise.level === 0 &&
50-
advise.method === 'getByRole' &&
49+
suggestion.level === 0 &&
50+
suggestion.method === 'getByRole' &&
5151
!data.name
5252
) {
53-
suggestion = (
53+
message = (
5454
<p>
5555
Awesome! This is great already! You could still make the query a bit
5656
more specific by adding the name option. This would require to add some
5757
markup though, as your element isn&apos;t named properly.
5858
</p>
5959
);
6060
} else if (
61-
advise.level === 0 &&
62-
advise.method === 'getByRole' &&
61+
suggestion.level === 0 &&
62+
suggestion.method === 'getByRole' &&
6363
data.name &&
6464
!hasNameArg
6565
) {
66-
suggestion = (
66+
message = (
6767
<p>
6868
There is one thing though. You could make the query a bit more specific
6969
by adding the name option.
7070
</p>
7171
);
7272
} else if (used.level > 0) {
73-
suggestion = (
73+
message = (
7474
<p>
7575
This isn&apos;t great, but we can&apos;t do better with the current
7676
markup. Extend your html to improve accessibility and unlock better
7777
queries.
7878
</p>
7979
);
8080
} else {
81-
suggestion = <p>This is great. Ship it!</p>;
81+
message = <p>This is great. Ship it!</p>;
8282
}
8383

8484
const handleClick = () => {
85-
jsEditorRef.current.setValue(advise.expression);
85+
jsEditorRef.current.setValue(suggestion.expression);
8686
};
8787

8888
return (
8989
<div className="space-y-4 text-sm">
9090
<div className={['text-white p-4 rounded space-y-2', color].join(' ')}>
9191
<div className="font-bold text-xs">suggested query</div>
92-
{advise.expression && (
92+
{suggestion.expression && (
9393
<div
9494
className="font-mono cursor-pointer text-xs"
9595
onClick={handleClick}
9696
>
97-
&gt; {advise.expression}
97+
&gt; {suggestion.expression}
98+
<br />
9899
</div>
99100
)}
100101
</div>
101-
<div className="min-h-8">{suggestion}</div>
102+
<div className="min-h-8">{message}</div>
102103
</div>
103104
);
104105
}

0 commit comments

Comments
 (0)