fizz test utils
一、作用
二、插入节点并执行脚本
async function insertNodesAndExecuteScripts(
source: Document | Element,
target: Node,
CSPnonce: string | null,
) {
const ownerDocument = target.ownerDocument || target;
// We need to remove the script content for any scripts that would not run based on CSP
// 我们需要删除任何根据 CSP 无法运行的脚本的脚本内容
// We restore the script content after moving the nodes into the target
// 在将节点移动到目标后,我们会恢复脚本内容
const badNonceScriptNodes: Map<Element, string> = new Map();
if (CSPnonce) {
const scripts = source.querySelectorAll('script');
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i];
if (
!script.hasAttribute('src') &&
script.getAttribute('nonce') !== CSPnonce
) {
badNonceScriptNodes.set(script, script.textContent);
script.textContent = '';
}
}
}
let lastChild = null;
while (source.firstChild) {
const node = source.firstChild;
if (lastChild === node) {
throw new Error('Infinite loop.');
}
lastChild = node;
if (node.nodeType === 1) {
const element: Element = node as any;
if (
element.dataset != null &&
(element.dataset.rxi != null ||
element.dataset.rri != null ||
element.dataset.rci != null ||
element.dataset.rsi != null)
) {
// Fizz external runtime instructions are expected to be in the body.
// Fizz 外部运行时指令预计应在主体中。
// When we have renderIntoContainer and renderDocument this will be
// more enforceable. At the moment you can misconfigure your stream and end up
// with instructions that are deep in the document
// 当我们有 renderIntoContainer 和 renderDocument 时,这将更容易执行。目前你可
// 能会错误配置你的流,从而导致指令深藏在文档中
(ownerDocument.body as any).appendChild(element);
} else {
target.appendChild(element);
if (element.nodeName === 'SCRIPT') {
await executeScript(element);
} else {
const scripts = element.querySelectorAll('script');
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i];
await executeScript(script);
}
}
}
} else {
target.appendChild(node);
}
}
// restore the textContent now that we have finished attempting to execute scripts
// 现在我们已经完成尝试执行脚本,恢复 textContent
badNonceScriptNodes.forEach((scriptContent, script) => {
script.textContent = scriptContent;
});
}
三、合并选项
function mergeOptions(options: Object, defaultOptions: Object): Object {
return {
...defaultOptions,
...options,
};
}
四、在节点中剥离外部运行时
function stripExternalRuntimeInNodes(
nodes: HTMLElement[] | HTMLCollection<HTMLElement>,
externalRuntimeSrc: string | null,
): HTMLElement[] {
if (!Array.isArray(nodes)) {
nodes = Array.from(nodes);
}
if (externalRuntimeSrc == null) {
return nodes;
}
return nodes.filter(
n =>
(n.tagName !== 'SCRIPT' && n.tagName !== 'script') ||
n.getAttribute('src') !== externalRuntimeSrc,
);
}
五、获取可见子元素
function getVisibleChildren(element: Element): React$Node {
const children = [];
let node: any = element.firstChild;
while (node) {
if (node.nodeType === 1) {
if (
((node.tagName !== 'SCRIPT' && node.tagName !== 'script') ||
node.hasAttribute('data-meaningful')) &&
node.tagName !== 'TEMPLATE' &&
node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&
!node.hasAttribute('aria-hidden') &&
// Ignore the render blocking expect
// 忽略渲染阻塞的期望
(node.getAttribute('rel') !== 'expect' ||
node.getAttribute('blocking') !== 'render')
) {
const props: any = {};
const attributes = node.attributes;
for (let i = 0; i < attributes.length; i++) {
if (
attributes[i].name === 'id' &&
attributes[i].value.includes(':')
) {
// We assume this is a React added ID that's a non-visual implementation detail.
// 我们假设这是一个 React 添加的 ID,它是一个非视觉的实现细节
continue;
}
props[attributes[i].name] = attributes[i].value;
}
const nestedChildren = getVisibleChildren(node);
if (nestedChildren !== undefined) {
props.children = nestedChildren;
}
children.push(
require('react').createElement(node.tagName.toLowerCase(), props),
);
}
} else if (node.nodeType === 3) {
children.push(node.data);
}
node = node.nextSibling;
}
return children.length === 0
? undefined
: children.length === 1
? children[0]
: children;
}
六、工具
1. 执行脚本
async function executeScript(script: Element) {
const ownerDocument = script.ownerDocument;
if (script.parentNode == null) {
throw new Error(
'executeScript expects to be called on script nodes that are currently in a document',
);
}
const parent = script.parentNode;
const scriptSrc = script.getAttribute('src');
if (scriptSrc) {
if (document !== ownerDocument) {
throw new Error(
'You must set the current document to the global document to use script src in tests',
);
}
try {
require(scriptSrc);
} catch (x) {
const event = new window.ErrorEvent('error', { error: x });
window.dispatchEvent(event);
}
} else {
const newScript = ownerDocument.createElement('script');
newScript.textContent = script.textContent;
// make sure to add nonce back to script if it exists
// 如果存在,确保将 nonce 添加回脚本
for (let i = 0; i < script.attributes.length; i++) {
const attribute = script.attributes[i];
newScript.setAttribute(attribute.name, attribute.value);
}
parent.insertBefore(newScript, script);
parent.removeChild(script);
}
}