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
23 changes: 5 additions & 18 deletions src/hooks/usePlayground.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,6 @@ const configureSandbox = (state) => {
};

const populateSandboxDebounced = debounce(populateSandbox, 250);
const parseDebounced = debounce((data, dispatch) => {
const result = parser.parse(data);
dispatch({ type: 'SET_RESULT', result });
}, 250);

const effectMap = {
UPDATE_SANDBOX: (state, effect, dispatch) => {
Expand All @@ -171,20 +167,10 @@ const effectMap = {
return;
}

const data = {
markup: state.markup,
query: state.query,
rootNode: state.rootNode,
prevResult: state.result,
};

if (state.settings.autoRun) {
populateSandboxDebounced(state, effect, dispatch);
parseDebounced(data, dispatch);
} else if (effect.immediate) {
populateSandbox(state, effect, dispatch);
const result = parser.parse(data);
dispatch({ type: 'SET_RESULT', result });
}
},

Expand Down Expand Up @@ -267,13 +253,10 @@ const effectMap = {
function getInitialState(props) {
const localSettings = JSON.parse(localStorage.getItem('playground_settings'));

let { instanceId } = props;

const state = {
...props,
status: 'loading',
dirty: false,
cacheId: instanceId,
settings: Object.assign(
{
autoRun: true,
Expand Down Expand Up @@ -328,17 +311,21 @@ function usePlayground(props) {
if (typeof onChange === 'function') {
onChange(state);
}
// ignore the exhaustive deps. We really want to call onChange with the full
// state object, but only when `state.result` has changed.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.result, onChange]);

// propagate sandbox ready/busy events to playground state
useEffect(() => {
const listener = ({ data: { source, type } }) => {
const listener = ({ data: { source, type, result } }) => {
if (source !== 'testing-playground-sandbox') {
return;
}

if (type === 'SANDBOX_READY') {
dispatch({ type: 'SET_STATUS', status: 'idle' });
dispatch({ type: 'SET_RESULT', result });
} else if (type === 'SANDBOX_BUSY') {
dispatch({ type: 'SET_STATUS', status: 'evaluating' });
}
Expand Down
103 changes: 4 additions & 99 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,99 +149,6 @@ function createEvaluator({ rootNode }) {
return { context, evaluator, exec, wrap };
}

function createSandbox({ markup }) {
// render the frame in a container, so we can set "display: none". If the
// hiding would be done in the frame itself, testing-library would mark the
// entire dom as being inaccessible. Now we don't have this problem :)
const container = document.createElement('div');
container.setAttribute(
'style',
'width: 1px; height: 1px; overflow: hidden; display: none;',
);

const frame = document.createElement('iframe');
frame.setAttribute('security', 'restricted');
frame.setAttribute('scrolling', 'no');
frame.setAttribute('frameBorder', '0');
frame.setAttribute('allowTransparency', 'true');
frame.setAttribute(
'sandbox',
'allow-same-origin allow-scripts allow-popups allow-forms',
);
frame.setAttribute(
'style',
'width: 800px; height: 600px; top: 0; left: 0px; border: 3px solid red;',
);
container.appendChild(frame);
document.body.appendChild(container);

const sandbox = frame.contentDocument || frame.contentWindow.document;
const { context, evaluator, wrap } = createEvaluator({
rootNode: sandbox.body,
});

const script = sandbox.createElement('script');
script.setAttribute('type', 'text/javascript');
script.innerHTML = `
window.exec = function exec(context, expr) {
const evaluator = ${evaluator};
return evaluator.apply(null, [...Object.values(context), (expr || '').trim()]);
}
`;

sandbox.head.appendChild(script);
sandbox.body.innerHTML = markup;

let body = markup;

// mock out userEvent in the fake sandbox
Object.keys(context.userEvent).map((x) => {
context.userEvent[x] = () => {};
});

return {
rootNode: sandbox.body,
ensureMarkup: (html) => {
if (body !== html) {
sandbox.body.innerHTML = html;
body = html;
}
},
eval: (query) =>
wrap(() => frame.contentWindow.exec(context, query), { markup, query }),
destroy: () => document.body.removeChild(container),
};
}

const sandboxes = {};

/**
* runInSandbox
*
* Create a sandbox in which the body element is populated with the
* provided html `markup`. The javascript `query` is injected into
* the document for evaluation.
*
* By providing a `cacheId`, the sandbox can be persisted. This
* allows one to reuse an instance, and thereby speed up successive
* queries.
*/
function runInSandbox({ markup, query, cacheId }) {
const sandbox = sandboxes[cacheId] || createSandbox({ markup });
sandbox.ensureMarkup(markup);

const result = sandbox.eval(query);

if (cacheId && !sandboxes[cacheId]) {
sandboxes[cacheId] = sandbox;
} else {
sandbox.destroy();
}

return result;
}

function runUnsafe({ rootNode, query }) {
const evaluator = createEvaluator({ rootNode });

Expand All @@ -260,14 +167,12 @@ function configure({ testIdAttribute }) {
testingLibraryConfigure({ testIdAttribute });
}

function parse({ rootNode, markup, query, cacheId, prevResult }) {
if (typeof markup !== 'string' && !rootNode) {
throw new Error('either markup or rootNode should be provided');
function parse({ rootNode, query, prevResult }) {
if (!rootNode) {
throw new Error(`rootNode should be provided`);
}

const result = rootNode
? runUnsafe({ rootNode, query })
: runInSandbox({ markup, query, cacheId });
const result = runUnsafe({ rootNode, query });

result.expression = getLastExpression(query);

Expand Down
16 changes: 14 additions & 2 deletions src/sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,20 @@ function onSelectNode(node, { origin }) {
function updateSandbox(rootNode, markup, query) {
postMessage({ type: 'SANDBOX_BUSY' });
setInnerHTML(rootNode, markup);
runQuery(rootNode, query);
postMessage({ type: 'SANDBOX_READY' });

// get and clean result
// eslint-disable-next-line no-unused-vars
const { markup: m, query: q, ...data } = runQuery(rootNode, query);

const result = {
...data,
accessibleRoles: Object.keys(data.accessibleRoles).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {}),
};

postMessage({ type: 'SANDBOX_READY', result });
}

function onMessage({ source, data }) {
Expand Down