Skip to content

Commit 89a0420

Browse files
authored
refactor: only parse queries in sandbox (#260)
1 parent dda36c9 commit 89a0420

File tree

3 files changed

+23
-119
lines changed

3 files changed

+23
-119
lines changed

src/hooks/usePlayground.js

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,6 @@ const configureSandbox = (state) => {
159159
};
160160

161161
const populateSandboxDebounced = debounce(populateSandbox, 250);
162-
const parseDebounced = debounce((data, dispatch) => {
163-
const result = parser.parse(data);
164-
dispatch({ type: 'SET_RESULT', result });
165-
}, 250);
166162

167163
const effectMap = {
168164
UPDATE_SANDBOX: (state, effect, dispatch) => {
@@ -171,20 +167,10 @@ const effectMap = {
171167
return;
172168
}
173169

174-
const data = {
175-
markup: state.markup,
176-
query: state.query,
177-
rootNode: state.rootNode,
178-
prevResult: state.result,
179-
};
180-
181170
if (state.settings.autoRun) {
182171
populateSandboxDebounced(state, effect, dispatch);
183-
parseDebounced(data, dispatch);
184172
} else if (effect.immediate) {
185173
populateSandbox(state, effect, dispatch);
186-
const result = parser.parse(data);
187-
dispatch({ type: 'SET_RESULT', result });
188174
}
189175
},
190176

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

270-
let { instanceId } = props;
271-
272256
const state = {
273257
...props,
274258
status: 'loading',
275259
dirty: false,
276-
cacheId: instanceId,
277260
settings: Object.assign(
278261
{
279262
autoRun: true,
@@ -328,17 +311,21 @@ function usePlayground(props) {
328311
if (typeof onChange === 'function') {
329312
onChange(state);
330313
}
314+
// ignore the exhaustive deps. We really want to call onChange with the full
315+
// state object, but only when `state.result` has changed.
316+
// eslint-disable-next-line react-hooks/exhaustive-deps
331317
}, [state.result, onChange]);
332318

333319
// propagate sandbox ready/busy events to playground state
334320
useEffect(() => {
335-
const listener = ({ data: { source, type } }) => {
321+
const listener = ({ data: { source, type, result } }) => {
336322
if (source !== 'testing-playground-sandbox') {
337323
return;
338324
}
339325

340326
if (type === 'SANDBOX_READY') {
341327
dispatch({ type: 'SET_STATUS', status: 'idle' });
328+
dispatch({ type: 'SET_RESULT', result });
342329
} else if (type === 'SANDBOX_BUSY') {
343330
dispatch({ type: 'SET_STATUS', status: 'evaluating' });
344331
}

src/parser.js

Lines changed: 4 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -149,99 +149,6 @@ function createEvaluator({ rootNode }) {
149149
return { context, evaluator, exec, wrap };
150150
}
151151

152-
function createSandbox({ markup }) {
153-
// render the frame in a container, so we can set "display: none". If the
154-
// hiding would be done in the frame itself, testing-library would mark the
155-
// entire dom as being inaccessible. Now we don't have this problem :)
156-
const container = document.createElement('div');
157-
container.setAttribute(
158-
'style',
159-
'width: 1px; height: 1px; overflow: hidden; display: none;',
160-
);
161-
162-
const frame = document.createElement('iframe');
163-
frame.setAttribute('security', 'restricted');
164-
frame.setAttribute('scrolling', 'no');
165-
frame.setAttribute('frameBorder', '0');
166-
frame.setAttribute('allowTransparency', 'true');
167-
frame.setAttribute(
168-
'sandbox',
169-
'allow-same-origin allow-scripts allow-popups allow-forms',
170-
);
171-
frame.setAttribute(
172-
'style',
173-
'width: 800px; height: 600px; top: 0; left: 0px; border: 3px solid red;',
174-
);
175-
container.appendChild(frame);
176-
document.body.appendChild(container);
177-
178-
const sandbox = frame.contentDocument || frame.contentWindow.document;
179-
const { context, evaluator, wrap } = createEvaluator({
180-
rootNode: sandbox.body,
181-
});
182-
183-
const script = sandbox.createElement('script');
184-
script.setAttribute('type', 'text/javascript');
185-
script.innerHTML = `
186-
window.exec = function exec(context, expr) {
187-
const evaluator = ${evaluator};
188-
189-
return evaluator.apply(null, [...Object.values(context), (expr || '').trim()]);
190-
}
191-
`;
192-
193-
sandbox.head.appendChild(script);
194-
sandbox.body.innerHTML = markup;
195-
196-
let body = markup;
197-
198-
// mock out userEvent in the fake sandbox
199-
Object.keys(context.userEvent).map((x) => {
200-
context.userEvent[x] = () => {};
201-
});
202-
203-
return {
204-
rootNode: sandbox.body,
205-
ensureMarkup: (html) => {
206-
if (body !== html) {
207-
sandbox.body.innerHTML = html;
208-
body = html;
209-
}
210-
},
211-
eval: (query) =>
212-
wrap(() => frame.contentWindow.exec(context, query), { markup, query }),
213-
destroy: () => document.body.removeChild(container),
214-
};
215-
}
216-
217-
const sandboxes = {};
218-
219-
/**
220-
* runInSandbox
221-
*
222-
* Create a sandbox in which the body element is populated with the
223-
* provided html `markup`. The javascript `query` is injected into
224-
* the document for evaluation.
225-
*
226-
* By providing a `cacheId`, the sandbox can be persisted. This
227-
* allows one to reuse an instance, and thereby speed up successive
228-
* queries.
229-
*/
230-
function runInSandbox({ markup, query, cacheId }) {
231-
const sandbox = sandboxes[cacheId] || createSandbox({ markup });
232-
sandbox.ensureMarkup(markup);
233-
234-
const result = sandbox.eval(query);
235-
236-
if (cacheId && !sandboxes[cacheId]) {
237-
sandboxes[cacheId] = sandbox;
238-
} else {
239-
sandbox.destroy();
240-
}
241-
242-
return result;
243-
}
244-
245152
function runUnsafe({ rootNode, query }) {
246153
const evaluator = createEvaluator({ rootNode });
247154

@@ -260,14 +167,12 @@ function configure({ testIdAttribute }) {
260167
testingLibraryConfigure({ testIdAttribute });
261168
}
262169

263-
function parse({ rootNode, markup, query, cacheId, prevResult }) {
264-
if (typeof markup !== 'string' && !rootNode) {
265-
throw new Error('either markup or rootNode should be provided');
170+
function parse({ rootNode, query, prevResult }) {
171+
if (!rootNode) {
172+
throw new Error(`rootNode should be provided`);
266173
}
267174

268-
const result = rootNode
269-
? runUnsafe({ rootNode, query })
270-
: runInSandbox({ markup, query, cacheId });
175+
const result = runUnsafe({ rootNode, query });
271176

272177
result.expression = getLastExpression(query);
273178

src/sandbox.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,20 @@ function onSelectNode(node, { origin }) {
119119
function updateSandbox(rootNode, markup, query) {
120120
postMessage({ type: 'SANDBOX_BUSY' });
121121
setInnerHTML(rootNode, markup);
122-
runQuery(rootNode, query);
123-
postMessage({ type: 'SANDBOX_READY' });
122+
123+
// get and clean result
124+
// eslint-disable-next-line no-unused-vars
125+
const { markup: m, query: q, ...data } = runQuery(rootNode, query);
126+
127+
const result = {
128+
...data,
129+
accessibleRoles: Object.keys(data.accessibleRoles).reduce((acc, key) => {
130+
acc[key] = true;
131+
return acc;
132+
}, {}),
133+
};
134+
135+
postMessage({ type: 'SANDBOX_READY', result });
124136
}
125137

126138
function onMessage({ source, data }) {

0 commit comments

Comments
 (0)