Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions src/components/Context.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
import React, { useContext, useCallback, useRef, useState } from 'react';
import React, { useContext, useRef, useState } from 'react';

export const AppContext = React.createContext();

function AppContextProvider(props) {
const jsEditorRef = useRef();
const htmlEditorRef = useRef();
const [htmlRoot, setHtmlRoot] = useState();
const [parsed, setParsed] = useState({});

const setHtmlRootRef = useCallback(
(node) => {
setHtmlRoot(node);
},
[setHtmlRoot],
);

return (
<AppContext.Provider
value={{
jsEditorRef,
htmlEditorRef,
htmlRoot,
setHtmlRootRef,
parsed,
setParsed,
}}
Expand Down
25 changes: 3 additions & 22 deletions src/components/Embedded.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ import MarkupEditor from './MarkupEditor';

const savedState = state.load();

const styles = {
offscreen: {
position: 'absolute',
left: -300,
width: 100,
},
};

const SUPPORTED_PANES = {
markup: true,
preview: true,
Expand All @@ -33,7 +25,7 @@ const SUPPORTED_PANES = {
function Embedded() {
const [html, setHtml] = useState(savedState.markup || initialValues.html);
const [js, setJs] = useState(savedState.query || initialValues.js);
const { setParsed, htmlRoot } = useAppContext();
const { setParsed } = useAppContext();

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

useEffect(() => {
if (!htmlRoot) {
return;
}

const parsed = parser.parse({ htmlRoot, js });
const parsed = parser.parse({ markup: html, query: js });
setParsed(parsed);

state.save({ markup: html, query: js });
state.updateTitle(parsed.expression?.expression);
}, [html, js, htmlRoot]);
}, [html, js]);

useEffect(() => {
document.body.classList.add('embedded');
Expand All @@ -78,13 +66,6 @@ function Embedded() {
<div
className={`h-full overflow-hidden grid grid-flow-col gap-4 p-4 bg-white shadow rounded ${columnClass}`}
>
{/*the markup preview must always be rendered!*/}
{!panes.includes('preview') && (
<div style={styles.offscreen}>
<Preview html={html} />
</div>
)}

{panes.map((area) => {
switch (area) {
case 'preview':
Expand Down
10 changes: 3 additions & 7 deletions src/components/Playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,15 @@ function Playground() {
const [html, setHtml] = useState(savedState.markup || initialValues.html);
const [js, setJs] = useState(savedState.query || initialValues.js);

const { setParsed, htmlRoot } = useAppContext();
const { setParsed } = useAppContext();

useEffect(() => {
if (!htmlRoot) {
return;
}

const parsed = parser.parse({ htmlRoot, js });
const parsed = parser.parse({ markup: html, query: js });
setParsed(parsed);

state.save({ markup: html, query: js });
state.updateTitle(parsed.expression?.expression);
}, [htmlRoot, html, js]);
}, [html, js]);

return (
<div className="flex flex-col h-auto md:h-full w-full">
Expand Down
44 changes: 28 additions & 16 deletions src/components/Preview.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useAppContext } from './Context';
import Scrollable from './Scrollable';
import PreviewHint from './PreviewHint';
import { getQueryAdvise } from '../lib';

function Preview({ html }) {
function selectByCssPath(rootNode, cssPath) {
return rootNode?.querySelector(cssPath.replace(/^body > /, ''));
}

function Preview() {
// Okay, listen up. `highlighted` can be a number of things, as I wanted to
// keep a single variable to represent the state. This to reduce bug count
// by creating out-of-sync states.
Expand All @@ -21,41 +25,49 @@ function Preview({ html }) {
// Indicating that the `parsed` element can be highlighted again.
const [highlighted, setHighlighted] = useState(false);
const [roles, setRoles] = useState([]);
const { parsed, jsEditorRef } = useAppContext();
const htmlRoot = useRef();

const { parsed, jsEditorRef, htmlRoot, setHtmlRootRef } = useAppContext();

const { advise } = getQueryAdvise({
root: htmlRoot ? htmlRoot.firstChild : null,
const { suggestion } = getQueryAdvise({
rootNode: htmlRoot.current ? htmlRoot.current.firstChild : null,
element: highlighted,
});

// TestingLibraryDom?.getSuggestedQuery(highlighted, 'get').toString() : null

useEffect(() => {
setRoles(Object.keys(parsed.roles || {}).sort());
}, [parsed.roles]);
setRoles(Object.keys(parsed.accessibleRoles || {}).sort());
}, [parsed.accessibleRoles]);

useEffect(() => {
if (highlighted) {
parsed.targets?.forEach((el) => el.classList.remove('highlight'));
parsed.elements?.forEach((el) => {
const target = selectByCssPath(htmlRoot.current, el.cssPath);
target?.classList.remove('highlight');
});
highlighted.classList?.add('highlight');
} else {
highlighted?.classList?.remove('highlight');

if (highlighted === false) {
parsed.targets?.forEach((el) => el.classList.add('highlight'));
parsed.elements?.forEach((el) => {
const target = selectByCssPath(htmlRoot.current, el.cssPath);
target?.classList.add('highlight');
});
}
}

return () => highlighted?.classList?.remove('highlight');
}, [highlighted, parsed.targets]);
}, [highlighted, parsed.elements]);

const handleClick = (event) => {
if (event.target === htmlRoot) {
if (event.target === htmlRoot.current) {
return;
}

event.preventDefault();
const expression =
advise.expression ||
suggestion.expression ||
'// No recommendation available.\n// Add some html attributes, or\n// use container.querySelector(…)';
jsEditorRef.current.setValue(expression);
};
Expand Down Expand Up @@ -84,15 +96,15 @@ function Preview({ html }) {
<Scrollable>
<div
className="preview"
ref={setHtmlRootRef}
onClick={handleClick}
onMouseMove={handleMove}
dangerouslySetInnerHTML={{ __html: html }}
ref={htmlRoot}
dangerouslySetInnerHTML={{ __html: parsed.markup }}
/>
</Scrollable>
</div>

<PreviewHint roles={roles} advise={advise} />
<PreviewHint roles={roles} suggestion={suggestion} />
</div>
);
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/PreviewHint.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import Expandable from './Expandable';

function PreviewHint({ roles, advise }) {
const expression = advise.expression ? (
`> ${advise.expression}`
function PreviewHint({ roles, suggestion }) {
const expression = suggestion.expression ? (
`> ${suggestion.expression}`
) : (
<>
<span className="font-bold">accessible roles: </span>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Query.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function Query({ onChange, initialValue }) {
<QueryEditor initialValue={initialValue} onChange={onChange} />
</div>

<QueryOutput error={parsed.error} result={parsed.text} />
<QueryOutput error={parsed.error?.message} result={parsed.formatted} />
</div>
);
}
Expand Down
21 changes: 11 additions & 10 deletions src/components/Result.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,32 @@ import ErrorBox from './ErrorBox';
import ResultQueries from './ResultQueries';
import ResultSuggestion from './ResultSuggestion';
import { useAppContext } from './Context';
import { getQueryAdvise } from '../lib';
import Scrollable from './Scrollable';

function Result() {
const { parsed, htmlRoot } = useAppContext();
const element = parsed.target;
const { parsed } = useAppContext();

if (parsed.error) {
return <ErrorBox caption={parsed.error} body={parsed.errorBody} />;
return (
<ErrorBox caption={parsed.error.message} body={parsed.error.details} />
);
}

const { data, advise } = getQueryAdvise({
root: htmlRoot,
element,
});
const { data, suggestion } = parsed.elements?.[0] || {};

if (!data || !suggestion) {
return <div />;
}

return (
<div className="flex flex-col overflow-hidden w-full h-full">
<div className="flex-none pb-4 border-b">
<ResultSuggestion data={data} advise={advise} />
<ResultSuggestion data={data} suggestion={suggestion} />
</div>

<div className="flex-auto">
<Scrollable>
<ResultQueries data={data} advise={advise} />
<ResultQueries data={data} suggestion={suggestion} />
</Scrollable>
</div>
</div>
Expand Down
47 changes: 24 additions & 23 deletions src/components/ResultSuggestion.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,97 +8,98 @@ function Code({ children }) {
return <span className="font-bold font-mono">{children}</span>;
}

function ResultSuggestion({ data, advise }) {
function ResultSuggestion({ data, suggestion }) {
const { parsed, jsEditorRef } = useAppContext();

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

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

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

const target = parsed.target || {};

let suggestion;
let message;

if (advise.level < used.level) {
suggestion = (
if (suggestion.level < used.level) {
message = (
<p>
You&apos;re using <Code>{used.method}</Code>, which falls under{' '}
<Code>{messages[used.level].heading}</Code>. Upgrading to{' '}
<Code>{advise.method}</Code> is recommended.
<Code>{suggestion.method}</Code> is recommended.
</p>
);
} else if (advise.level === 0 && advise.method !== used.method) {
suggestion = (
} else if (suggestion.level === 0 && suggestion.method !== used.method) {
message = (
<p>
Nice! <Code>{used.method}</Code> is a great selector! Using{' '}
<Code>{advise.method}</Code> would still be preferable though.
<Code>{suggestion.method}</Code> would still be preferable though.
</p>
);
} else if (target.tagName === 'INPUT' && !target.getAttribute('type')) {
suggestion = (
message = (
<p>
You can unlock <Code>getByRole</Code> by adding the{' '}
<Code>type=&quot;text&quot;</Code> attribute explicitly. Accessibility
will benefit from it.
</p>
);
} else if (
advise.level === 0 &&
advise.method === 'getByRole' &&
suggestion.level === 0 &&
suggestion.method === 'getByRole' &&
!data.name
) {
suggestion = (
message = (
<p>
Awesome! This is great already! You could still make the query a bit
more specific by adding the name option. This would require to add some
markup though, as your element isn&apos;t named properly.
</p>
);
} else if (
advise.level === 0 &&
advise.method === 'getByRole' &&
suggestion.level === 0 &&
suggestion.method === 'getByRole' &&
data.name &&
!hasNameArg
) {
suggestion = (
message = (
<p>
There is one thing though. You could make the query a bit more specific
by adding the name option.
</p>
);
} else if (used.level > 0) {
suggestion = (
message = (
<p>
This isn&apos;t great, but we can&apos;t do better with the current
markup. Extend your html to improve accessibility and unlock better
queries.
</p>
);
} else {
suggestion = <p>This is great. Ship it!</p>;
message = <p>This is great. Ship it!</p>;
}

const handleClick = () => {
jsEditorRef.current.setValue(advise.expression);
jsEditorRef.current.setValue(suggestion.expression);
};

return (
<div className="space-y-4 text-sm">
<div className={['text-white p-4 rounded space-y-2', color].join(' ')}>
<div className="font-bold text-xs">suggested query</div>
{advise.expression && (
{suggestion.expression && (
<div
className="font-mono cursor-pointer text-xs"
onClick={handleClick}
>
&gt; {advise.expression}
&gt; {suggestion.expression}
<br />
</div>
)}
</div>
<div className="min-h-8">{suggestion}</div>
<div className="min-h-8">{message}</div>
</div>
);
}
Expand Down
Loading