feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,814 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders admonition node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="admonition">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<div data-v-c8910e11="" data-v-8dfe8a80="" class="admonition admonition-stable">
|
||||
<!-- Legend-style title that sits on the border -->
|
||||
<div data-v-c8910e11="" id="admonition-stable" class="admonition-stable">
|
||||
<!-- SVG icons per type --><svg data-v-c8910e11="" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="admonition-stable">
|
||||
<path data-v-c8910e11="" d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path>
|
||||
<path data-v-c8910e11="" d="M12 9v4"></path>
|
||||
<path data-v-c8910e11="" d="M12 17h.01"></path>
|
||||
</svg><span data-v-c8910e11="" class="admonition-stable">Warning</span>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
<div data-v-c8910e11="" id="admonition-stable-content" class="admonition-stable" aria-labelledby="admonition-stable">
|
||||
<div data-v-8dfe8a80="" data-v-c8910e11="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="admonition-stable-renderer-0-0-0"><span data-v-a84b8095="">Admonition content</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders blockquote node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="blockquote">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<blockquote data-v-8ae59609="" data-v-8dfe8a80="" class="blockquote" dir="auto" is-dark="false">
|
||||
<div data-v-8dfe8a80="" data-v-8ae59609="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="blockquote-markdown-renderer-0-0-0"><span data-v-a84b8095="">Blockquote rendered content.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</blockquote>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders checkbox nodes 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="list">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<ul data-v-b195669a="" data-v-8dfe8a80="" class="list-node list-disc" is-dark="false">
|
||||
<li data-v-d94d2511="" data-v-b195669a="" class="list-item" dir="auto">
|
||||
<div data-v-8dfe8a80="" data-v-d94d2511="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-31e4c290="" data-v-93ee5c2d="" class="checkbox-node" role="img" aria-label="checked" index-key="list-item-markdown-renderer-0-0-0-0"><!-- Unchecked --><!-- Checked --><svg data-v-31e4c290="" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" class="checkbox-icon checkbox-checked"><rect data-v-31e4c290="" x="3" y="3" width="18" height="18" rx="4" fill="currentColor"></rect><path data-v-31e4c290="" d="M9 12l2 2 4-4" stroke="hsl(var(--ms-background))" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"></path></svg></span>
|
||||
<!----><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-0-0-2"><span data-v-a84b8095=""> Completed task</span>
|
||||
<!--v-if--></span>
|
||||
<!---->
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</li>
|
||||
<li data-v-d94d2511="" data-v-b195669a="" class="list-item" dir="auto">
|
||||
<div data-v-8dfe8a80="" data-v-d94d2511="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-31e4c290="" data-v-93ee5c2d="" class="checkbox-node" role="img" aria-label="unchecked" index-key="list-item-markdown-renderer-0-1-0-0"><!-- Unchecked --><svg data-v-31e4c290="" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" class="checkbox-icon checkbox-unchecked"><rect data-v-31e4c290="" x="3" y="3" width="18" height="18" rx="4" stroke="currentColor" stroke-width="2"></rect></svg></span>
|
||||
<!----><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-1-0-2"><span data-v-a84b8095=""> Pending task</span>
|
||||
<!--v-if--></span>
|
||||
<!---->
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders code block node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="code_block">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals --><pre data-v-8dfe8a80="" class="language-ts" aria-busy="false" aria-label="Code block: ts" data-language="ts" data-markstream-pre="1" tabindex="0" loading="false" index-key="markdown-renderer-0" stream="true" showtooltips="true" is-dark="false"><code translate="no">export const sum = (a: number, b: number) => a + b
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders complex inline code with emphasis and multiple code spans 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="list">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<ol data-v-b195669a="" data-v-8dfe8a80="" class="list-node list-decimal" is-dark="false">
|
||||
<li data-v-d94d2511="" data-v-b195669a="" class="list-item" dir="auto" value="1">
|
||||
<div data-v-8dfe8a80="" data-v-d94d2511="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><strong data-v-b1e65240="" data-v-93ee5c2d="" class="strong-node"><span data-v-a84b8095="" data-v-b1e65240="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-0-0-0-0"><span data-v-a84b8095="">整合冗余条款:</span><!--v-if--></span></strong><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-0-0-1"><span data-v-a84b8095=""> 将原</span>
|
||||
<!--v-if--></span><code data-v-36040df8="" data-v-93ee5c2d="" class="inline-code" index-key="list-item-markdown-renderer-0-0-0-2"><span data-v-36040df8="">1-(5)</span><!--v-if--></code><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-0-0-3"><span data-v-a84b8095="">、</span>
|
||||
<!--v-if--></span><code data-v-36040df8="" data-v-93ee5c2d="" class="inline-code" index-key="list-item-markdown-renderer-0-0-0-4"><span data-v-36040df8="">3-(3)</span><!--v-if--></code><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-0-0-5"><span data-v-a84b8095="">、</span>
|
||||
<!--v-if--></span><code data-v-36040df8="" data-v-93ee5c2d="" class="inline-code" index-key="list-item-markdown-renderer-0-0-0-6"><span data-v-36040df8="">3-(4)</span><!--v-if--></code><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-0-0-7"><span data-v-a84b8095="">中关于“缴纳费用”和“告知法律状态变化”的核心义务合并,统一放入</span>
|
||||
<!--v-if--></span><code data-v-36040df8="" data-v-93ee5c2d="" class="inline-code" index-key="list-item-markdown-renderer-0-0-0-8"><span data-v-36040df8="">1-(4)</span><!--v-if--></code><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-0-0-9"><span data-v-a84b8095="">“法律状态维护与通知义务”中,使其逻辑更集中,避免了在不同部分重复提及相似义务。</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders definition list node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="definition_list">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<dl data-v-084c0831="" data-v-8dfe8a80="" class="definition-list" is-dark="false">
|
||||
<dt data-v-084c0831="" class="definition-term">
|
||||
<div data-v-8dfe8a80="" data-v-084c0831="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="text">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true"><span data-v-a84b8095="" data-v-8dfe8a80="" class="whitespace-pre-wrap break-words text-node" index-key="definition-term-markdown-renderer-0-0-0" typewriter="true" is-dark="false"><span data-v-a84b8095="">Term</span>
|
||||
<!--v-if--></span>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</dt>
|
||||
<dd data-v-084c0831="" class="definition-desc">
|
||||
<div data-v-8dfe8a80="" data-v-084c0831="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="definition-desc-markdown-renderer-0-0-0-0"><span data-v-a84b8095="">Definition details</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders emoji node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Smile with </span>
|
||||
<!--v-if--></span><span data-v-b001e831="" data-v-93ee5c2d="" class="emoji-node" index-key="markdown-renderer-0-1">😄</span><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095=""> emoji.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders emphasis node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">This is </span>
|
||||
<!--v-if--></span><em data-v-ca49685e="" data-v-93ee5c2d="" class="emphasis-node"><span data-v-a84b8095="" data-v-ca49685e="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-1-0"><span data-v-a84b8095="">italic text</span><!--v-if--></span></em><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095="">.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders footnote nodes 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">A footnote reference</span>
|
||||
<!--v-if--></span><sup data-v-ba0412bc="" data-v-93ee5c2d="" id="fnref-1" class="footnote-reference" index-key="markdown-renderer-0-1"><span data-v-ba0412bc="" href="#footnote-1" title="查看脚注 1" class="footnote-link cursor-pointer">[1]</span></sup><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095="">.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="1" data-node-type="footnote">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<div data-v-8dfe8a80="" id="fnref--1" class="footnote-node flex text-sm leading-relaxed border-t border-[var(--footnote-border)] pt-2" is-dark="false">
|
||||
<!-- <span class="font-semibold mr-2 text-[#0366d6]">[{{ node.id }}]</span> -->
|
||||
<div class="flex-1">
|
||||
<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="footnote-markdown-renderer-1-0-0"><span data-v-a84b8095="">Footnote explanation</span>
|
||||
<!--v-if--></span><a data-v-31914872="" data-v-93ee5c2d="" class="footnote-anchor text-sm hover:underline cursor-pointer" href="#fnref-1" title="返回引用 1" index-key="footnote-markdown-renderer-1-0-1"> ↩︎ </a>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders footnote reference node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="footnote_reference">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true"><sup data-v-ba0412bc="" data-v-8dfe8a80="" id="fnref-1" class="footnote-reference" index-key="markdown-renderer-0" typewriter="true" is-dark="false"><span data-v-ba0412bc="" href="#footnote-1" title="查看脚注 1" class="footnote-link cursor-pointer">[1]</span></sup></transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders hardbreak node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Line one</span>
|
||||
<!--v-if--></span><br data-v-2587360c="" data-v-93ee5c2d="" class="hard-break" index-key="markdown-renderer-0-1"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095="">Line two</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders heading node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="heading">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<h1 data-v-9f883140="" data-v-8dfe8a80="" class="heading-node heading-1" dir="auto" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-9f883140="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Heading Node Title</span>
|
||||
<!--v-if--></span>
|
||||
</h1>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders highlight node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Use </span>
|
||||
<!--v-if--></span><mark data-v-9bc40b1b="" data-v-93ee5c2d="" class="highlight-node"><span data-v-a84b8095="" data-v-9bc40b1b="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-1-0"><span data-v-a84b8095="">highlighted text</span>
|
||||
<!--v-if--></span>
|
||||
</mark><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095=""> for emphasis.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders image links with relative target stay inline 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><a data-v-994bfd08="" data-v-93ee5c2d="" class="link-node" href="https://www.npmjs.com/package/markstream-vue" title="" aria-label="Link: https://www.npmjs.com/package/markstream-vue" aria-hidden="false" target="_blank" rel="noopener noreferrer" style="--underline-height: 2px; --underline-bottom: -3px; --underline-opacity: 0.35; --underline-rest-opacity: 0.175; --underline-duration: 1.6s; --underline-timing: ease-in-out; --underline-iteration: infinite;"><span data-v-4ff2ac62="" data-v-994bfd08="" class="image-node-container" index-key="markdown-renderer-0-0-0"><img data-v-4ff2ac62="" src="https://img.shields.io/npm/v/markstream-vue?color=a1b858&label=" alt="NPM version" title="NPM version" class="image-node__img is-loaded" fetchpriority="high" decoding="sync" tabindex="-1" aria-label="NPM version"></span></a> <a data-v-994bfd08="" data-v-93ee5c2d="" class="link-node" href="README.zh-CN.md" title="" aria-label="Link: README.zh-CN.md" aria-hidden="false" target="_blank" rel="noopener noreferrer" style="--underline-height: 2px; --underline-bottom: -3px; --underline-opacity: 0.35; --underline-rest-opacity: 0.175; --underline-duration: 1.6s; --underline-timing: ease-in-out; --underline-iteration: infinite;"><span data-v-4ff2ac62="" data-v-994bfd08="" class="image-node-container" index-key="markdown-renderer-0-2-0"><img data-v-4ff2ac62="" src="https://img.shields.io/badge/docs-%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%A3-blue" alt="中文版" title="中文版" class="image-node__img is-loaded" fetchpriority="high" decoding="sync" tabindex="-1" aria-label="中文版"></span></a></p>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders image node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-4ff2ac62="" data-v-93ee5c2d="" class="image-node-container" index-key="markdown-renderer-0-0"><img data-v-4ff2ac62="" src="https://example.com/vue.png" alt="Vue Logo" title="Vue Logo" class="image-node__img is-loaded" fetchpriority="high" decoding="sync" tabindex="-1" aria-label="Vue Logo"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders inline code node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Here is </span>
|
||||
<!--v-if--></span><code data-v-36040df8="" data-v-93ee5c2d="" class="inline-code" index-key="markdown-renderer-0-1"><span data-v-36040df8="">const answer = 42</span><!--v-if--></code><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095=""> inline.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders insert node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Here comes </span>
|
||||
<!--v-if--></span><ins data-v-1e673445="" data-v-93ee5c2d="" class="insert-node"><span data-v-a84b8095="" data-v-1e673445="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-1-0"><span data-v-a84b8095="">inserted text</span>
|
||||
<!--v-if--></span>
|
||||
</ins><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095=""> in the paragraph.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders link node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Visit </span>
|
||||
<!--v-if--></span><a data-v-994bfd08="" data-v-93ee5c2d="" class="link-node" href="https://vuejs.org" title="" aria-label="Link: https://vuejs.org" aria-hidden="false" target="_blank" rel="noopener noreferrer" style="--underline-height: 2px; --underline-bottom: -3px; --underline-opacity: 0.35; --underline-rest-opacity: 0.175; --underline-duration: 1.6s; --underline-timing: ease-in-out; --underline-iteration: infinite;"><span data-v-a84b8095="" data-v-994bfd08="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-1-0"><span data-v-a84b8095="">Vue</span>
|
||||
<!--v-if--></span>
|
||||
</a><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095=""> now.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders math inline node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Einstein wrote </span>
|
||||
<!--v-if--></span><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-1"><span data-v-a84b8095="">$E=mc^2$</span>
|
||||
<!--v-if--></span><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095="">.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders ordered list node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="list">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<ol data-v-b195669a="" data-v-8dfe8a80="" class="list-node list-decimal" is-dark="false">
|
||||
<li data-v-d94d2511="" data-v-b195669a="" class="list-item" dir="auto" value="1">
|
||||
<div data-v-8dfe8a80="" data-v-d94d2511="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-0-0-0"><span data-v-a84b8095="">First entry</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</li>
|
||||
<li data-v-d94d2511="" data-v-b195669a="" class="list-item" dir="auto" value="2">
|
||||
<div data-v-8dfe8a80="" data-v-d94d2511="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-1-0-0"><span data-v-a84b8095="">Second entry</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders paragraph and text node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Simple paragraph content rendered as plain text.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders reference node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Cite research </span>
|
||||
<!--v-if--></span><span data-v-7c86ecd2="" data-v-93ee5c2d="" class="reference-node cursor-pointer text-xs rounded-md px-1.5 mx-0.5" role="button" tabindex="0" index-key="markdown-renderer-0-1">1</span><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095=""> for details.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders single badge image with trailing newline stays inline 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-4ff2ac62="" data-v-93ee5c2d="" class="image-node-container" index-key="markdown-renderer-0-0"><img data-v-4ff2ac62="" src="https://img.shields.io/npm/l/markstream-vue" alt="License" title="License" class="image-node__img is-loaded" fetchpriority="high" decoding="sync" tabindex="-1" aria-label="License"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders strikethrough node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">This is </span>
|
||||
<!--v-if--></span><del data-v-d7c2e36d="" data-v-93ee5c2d="" class="strikethrough-node"><span data-v-a84b8095="" data-v-d7c2e36d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-1-0"><span data-v-a84b8095="">deleted text</span>
|
||||
<!--v-if--></span>
|
||||
</del><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095="">.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders strong node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">This is </span>
|
||||
<!--v-if--></span><strong data-v-b1e65240="" data-v-93ee5c2d="" class="strong-node"><span data-v-a84b8095="" data-v-b1e65240="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-1-0"><span data-v-a84b8095="">bold text</span><!--v-if--></span></strong><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095="">.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders subscript node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Chemical formula H</span>
|
||||
<!--v-if--></span><sub data-v-40ad1ac6="" data-v-93ee5c2d="" class="subscript-node"><span data-v-a84b8095="" data-v-40ad1ac6="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-1-0"><span data-v-a84b8095="">2</span>
|
||||
<!--v-if--></span>
|
||||
</sub><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095="">O is familiar.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders superscript node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">Math uses x</span>
|
||||
<!--v-if--></span><sup data-v-9c4a3c09="" data-v-93ee5c2d="" class="superscript-node"><span data-v-a84b8095="" data-v-9c4a3c09="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-1-0"><span data-v-a84b8095="">2</span>
|
||||
<!--v-if--></span>
|
||||
</sup><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-2"><span data-v-a84b8095=""> frequently.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders table node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="table">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<div data-v-8fb37ab3="" data-v-8dfe8a80="" class="table-node-wrapper" loading="false">
|
||||
<table data-v-8fb37ab3="" class="table-node" aria-busy="false">
|
||||
<!--v-if-->
|
||||
<thead data-v-8fb37ab3="">
|
||||
<tr data-v-8fb37ab3="">
|
||||
<th data-v-8fb37ab3="" dir="auto" class="text-left">
|
||||
<div data-v-8dfe8a80="" data-v-8fb37ab3="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="text">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true"><span data-v-a84b8095="" data-v-8dfe8a80="" class="whitespace-pre-wrap break-words text-node" index-key="table-th-markdown-renderer-0-0" typewriter="true" is-dark="false"><span data-v-a84b8095="">Name</span>
|
||||
<!--v-if--></span>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div><button data-v-8fb37ab3="" type="button" class="table-node__resize-handle" aria-label="Resize columns 1 and 2"></button>
|
||||
</th>
|
||||
<th data-v-8fb37ab3="" dir="auto" class="text-left">
|
||||
<div data-v-8dfe8a80="" data-v-8fb37ab3="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="text">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true"><span data-v-a84b8095="" data-v-8dfe8a80="" class="whitespace-pre-wrap break-words text-node" index-key="table-th-markdown-renderer-0-0" typewriter="true" is-dark="false"><span data-v-a84b8095="">Role</span>
|
||||
<!--v-if--></span>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-v-8fb37ab3="">
|
||||
<tr data-v-8fb37ab3="">
|
||||
<td data-v-8fb37ab3="" class="text-left" dir="auto">
|
||||
<div data-v-8dfe8a80="" data-v-8fb37ab3="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="text">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true"><span data-v-a84b8095="" data-v-8dfe8a80="" class="whitespace-pre-wrap break-words text-node" index-key="table-td-markdown-renderer-0-0" typewriter="true" is-dark="false"><span data-v-a84b8095="">Alice</span>
|
||||
<!--v-if--></span>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</td>
|
||||
<td data-v-8fb37ab3="" class="text-left" dir="auto">
|
||||
<div data-v-8dfe8a80="" data-v-8fb37ab3="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="text">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true"><span data-v-a84b8095="" data-v-8dfe8a80="" class="whitespace-pre-wrap break-words text-node" index-key="table-td-markdown-renderer-0-0" typewriter="true" is-dark="false"><span data-v-a84b8095="">Developer</span>
|
||||
<!--v-if--></span>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<transition-stub data-v-8fb37ab3="" name="table-node-fade" appear="false" persisted="false" css="true">
|
||||
<!--v-if-->
|
||||
</transition-stub>
|
||||
</div>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders thematic break node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-0-0"><span data-v-a84b8095="">First section.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="1" data-node-type="thematic_break">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<hr data-v-18e4f209="" data-v-8dfe8a80="" class="hr-node" node="[object Object]" index-key="markdown-renderer-1" typewriter="true" is-dark="false">
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="2" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="markdown-renderer-2-0"><span data-v-a84b8095="">Second section.</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`markdownRender node e2e coverage > renders unordered list node 1`] = `
|
||||
"<div data-v-8dfe8a80="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="list">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<ul data-v-b195669a="" data-v-8dfe8a80="" class="list-node list-disc" is-dark="false">
|
||||
<li data-v-d94d2511="" data-v-b195669a="" class="list-item" dir="auto">
|
||||
<div data-v-8dfe8a80="" data-v-d94d2511="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-0-0-0"><span data-v-a84b8095="">Item one</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</li>
|
||||
<li data-v-d94d2511="" data-v-b195669a="" class="list-item" dir="auto">
|
||||
<div data-v-8dfe8a80="" data-v-d94d2511="" class="markstream-vue markdown-renderer">
|
||||
<!--v-if-->
|
||||
<div data-v-8dfe8a80="" class="node-slot" data-node-index="0" data-node-type="paragraph">
|
||||
<div data-v-8dfe8a80="" class="node-content">
|
||||
<!-- Skip wrapping code_block nodes in transitions to avoid touching Monaco editor internals -->
|
||||
<transition-stub data-v-8dfe8a80="" name="typewriter" appear="true" persisted="false" css="true">
|
||||
<p data-v-93ee5c2d="" data-v-8dfe8a80="" dir="auto" class="paragraph-node" typewriter="true" is-dark="false"><span data-v-a84b8095="" data-v-93ee5c2d="" class="whitespace-pre-wrap break-words text-node" index-key="list-item-markdown-renderer-0-1-0-0"><span data-v-a84b8095="">Item two</span>
|
||||
<!--v-if--></span>
|
||||
</p>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import Demo from '../../playground/src/pages/mermaid-export-demo.vue'
|
||||
import MermaidBlockNode from '../../src/components/MermaidBlockNode/MermaidBlockNode.vue'
|
||||
|
||||
describe('playground mermaid-export demo', () => {
|
||||
it('intercepts export and displays uploaded URL', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
const wrapper = mount(Demo as any, { attachTo: document.body })
|
||||
|
||||
// Wait for mounted lifecycle
|
||||
await nextTick()
|
||||
|
||||
// Find the rendered MermaidBlockNode instance
|
||||
const m = wrapper.findComponent(MermaidBlockNode as any)
|
||||
expect(m.exists()).toBeTruthy()
|
||||
|
||||
// Ensure the export button will be shown by setting mermaidAvailable
|
||||
;(m.vm as any).mermaidAvailable = true
|
||||
;(m.vm as any).showSource = false
|
||||
await nextTick()
|
||||
|
||||
// Inject a fake SVG into the mermaid content area so export handler has something
|
||||
const content = m.find('div._mermaid')
|
||||
expect(content.exists()).toBeTruthy()
|
||||
content.element.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg"><g></g></svg>'
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Instead of relying on DOM click wiring, emit the `export` event directly on the child
|
||||
const ev = {
|
||||
payload: { type: 'export' },
|
||||
defaultPrevented: false,
|
||||
preventDefault() { this.defaultPrevented = true },
|
||||
svgElement: content.element.querySelector('svg'),
|
||||
svgString: '<svg></svg>',
|
||||
}
|
||||
;(m.vm as any).$emit('export', ev)
|
||||
|
||||
// Advance timers to resolve the component's fake upload (700ms)
|
||||
vi.runAllTimers()
|
||||
// allow microtasks scheduled by async handlers to run
|
||||
await Promise.resolve()
|
||||
await nextTick()
|
||||
|
||||
// The uploaded URL should be displayed by the demo
|
||||
expect(wrapper.html()).toContain('Uploaded')
|
||||
const link = wrapper.find('a')
|
||||
expect(link.exists()).toBeTruthy()
|
||||
expect(link.attributes('href') || '').toMatch(/^https:\/\/example\.com\/uploads/)
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,997 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { full as markdownItEmoji } from 'markdown-it-emoji'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { removeCustomComponents, setCustomComponents } from '../../src/utils/nodeComponents'
|
||||
import { flushAll } from '../setup/flush-all'
|
||||
|
||||
interface Scenario {
|
||||
name: string
|
||||
markdown: string
|
||||
expectedText?: string | string[]
|
||||
props?: Record<string, any>
|
||||
mountOptions?: Record<string, any>
|
||||
assert?: (wrapper: VueWrapper<any>) => void | Promise<void>
|
||||
skipSnapshot?: boolean
|
||||
}
|
||||
|
||||
let MarkdownRender: any
|
||||
|
||||
// Use shared flushAll from test/setup/flush-all
|
||||
|
||||
async function mountMarkdown(markdown: string, props: Record<string, any> = {}, mountOptions: Record<string, any> = {}) {
|
||||
const wrapper = mount(MarkdownRender, {
|
||||
props: {
|
||||
content: markdown,
|
||||
...props,
|
||||
customMarkdownIt: (md: any) => md.use(markdownItEmoji),
|
||||
},
|
||||
...mountOptions,
|
||||
})
|
||||
await flushAll()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
function normalizeText(input: string) {
|
||||
return input.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function sanitizeSnapshotHtml(html: string, name: string) {
|
||||
html = html.replace(/class="([^"]*)"/g, (_, classNames: string) => {
|
||||
const normalized = classNames
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.filter(className => !/^typewriter-(?:enter|leave)(?:-(?:from|to|active))?$/.test(className))
|
||||
.join(' ')
|
||||
|
||||
return normalized ? `class="${normalized}"` : ''
|
||||
})
|
||||
|
||||
if (name.includes('admonition'))
|
||||
return html.replace(/admonition-[a-z0-9]+/gi, 'admonition-stable')
|
||||
return html
|
||||
}
|
||||
|
||||
describe('markdownRender node e2e coverage', () => {
|
||||
beforeAll(async () => {
|
||||
vi.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000)
|
||||
MarkdownRender = (await import('../../src/components/NodeRenderer')).default
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
; (Date.now as any).mockRestore?.()
|
||||
})
|
||||
|
||||
const scenarios: Scenario[] = [
|
||||
{
|
||||
name: 'paragraph and text node',
|
||||
markdown: 'Simple paragraph content rendered as plain text.',
|
||||
expectedText: 'Simple paragraph content rendered as plain text.',
|
||||
assert: (wrapper) => {
|
||||
const paragraph = wrapper.find('p')
|
||||
expect(paragraph.exists()).toBe(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'heading node',
|
||||
markdown: '# Heading Node Title',
|
||||
expectedText: 'Heading Node Title',
|
||||
assert: (wrapper) => {
|
||||
const heading = wrapper.find('h1')
|
||||
expect(heading.exists()).toBe(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'blockquote node',
|
||||
markdown: '> Blockquote rendered content.',
|
||||
expectedText: 'Blockquote rendered content.',
|
||||
assert: (wrapper) => {
|
||||
const blockquote = wrapper.find('blockquote')
|
||||
expect(blockquote.exists()).toBe(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'unordered list node',
|
||||
markdown: '- Item one\n- Item two',
|
||||
expectedText: ['Item one', 'Item two'],
|
||||
assert: (wrapper) => {
|
||||
const list = wrapper.find('ul')
|
||||
expect(list.exists()).toBe(true)
|
||||
expect(list.findAll('li')).toHaveLength(2)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ordered list node',
|
||||
markdown: '1. First entry\n2. Second entry',
|
||||
expectedText: ['First entry', 'Second entry'],
|
||||
assert: (wrapper) => {
|
||||
const list = wrapper.find('ol')
|
||||
expect(list.exists()).toBe(true)
|
||||
expect(list.findAll('li')).toHaveLength(2)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'checkbox nodes',
|
||||
markdown: '- [x] Completed task\n- [ ] Pending task',
|
||||
expectedText: ['Completed task', 'Pending task'],
|
||||
assert: (wrapper) => {
|
||||
const checkboxes = wrapper.findAll('.checkbox-node')
|
||||
expect(checkboxes).toHaveLength(2)
|
||||
expect(checkboxes[0].find('.checkbox-checked').exists()).toBe(true)
|
||||
expect(checkboxes[1].find('.checkbox-unchecked').exists()).toBe(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'link node',
|
||||
markdown: 'Visit [Vue](https://vuejs.org) now.',
|
||||
expectedText: ['Visit', 'Vue', 'now.'],
|
||||
assert: (wrapper) => {
|
||||
const link = wrapper.find('a[href="https://vuejs.org"]')
|
||||
expect(link.exists()).toBe(true)
|
||||
expect(link.text()).toBe('Vue')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'image node',
|
||||
markdown: '',
|
||||
mountOptions: {
|
||||
global: {
|
||||
stubs: {
|
||||
'transition': false,
|
||||
'transition-group': false,
|
||||
},
|
||||
},
|
||||
},
|
||||
assert: (wrapper) => {
|
||||
const paragraph = wrapper.find('p.paragraph-node')
|
||||
expect(paragraph.exists()).toBe(true)
|
||||
expect(paragraph.find('figure').exists()).toBe(false)
|
||||
expect(paragraph.findAll('img')).toHaveLength(1)
|
||||
const img = paragraph.find('img')
|
||||
expect(img.exists()).toBe(true)
|
||||
expect(img.attributes('src')).toBe('https://example.com/vue.png')
|
||||
expect(img.attributes('title')).toBe('Vue Logo')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'single badge image with trailing newline stays inline',
|
||||
markdown: '\n',
|
||||
mountOptions: {
|
||||
global: {
|
||||
stubs: {
|
||||
'transition': false,
|
||||
'transition-group': false,
|
||||
},
|
||||
},
|
||||
},
|
||||
assert: (wrapper) => {
|
||||
const paragraph = wrapper.find('p.paragraph-node')
|
||||
expect(paragraph.exists()).toBe(true)
|
||||
expect(paragraph.find('figure').exists()).toBe(false)
|
||||
expect(paragraph.findAll('img')).toHaveLength(1)
|
||||
const img = paragraph.find('img')
|
||||
expect(img.exists()).toBe(true)
|
||||
expect(img.attributes('src')).toBe('https://img.shields.io/npm/l/markstream-vue')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'image paragraph separated by blank lines stays inline',
|
||||
markdown: `Before
|
||||
|
||||

|
||||
|
||||
After`,
|
||||
expectedText: ['Before', 'After'],
|
||||
mountOptions: {
|
||||
global: {
|
||||
stubs: {
|
||||
'transition': false,
|
||||
'transition-group': false,
|
||||
},
|
||||
},
|
||||
},
|
||||
skipSnapshot: true,
|
||||
assert: (wrapper) => {
|
||||
const paragraphs = wrapper.findAll('p.paragraph-node')
|
||||
expect(paragraphs).toHaveLength(3)
|
||||
const imageParagraph = paragraphs[1]
|
||||
expect(imageParagraph.find('figure').exists()).toBe(false)
|
||||
expect(imageParagraph.findAll('img')).toHaveLength(1)
|
||||
const img = imageParagraph.find('img')
|
||||
expect(img.exists()).toBe(true)
|
||||
expect(img.attributes('src')).toBe('https://example.com/vue.png')
|
||||
expect(img.attributes('title')).toBe('Vue Logo')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'inline image inside text stays inline',
|
||||
markdown: 'Before  after.',
|
||||
expectedText: ['Before', 'after.'],
|
||||
mountOptions: {
|
||||
global: {
|
||||
stubs: {
|
||||
'transition': false,
|
||||
'transition-group': false,
|
||||
},
|
||||
},
|
||||
},
|
||||
skipSnapshot: true,
|
||||
assert: (wrapper) => {
|
||||
const paragraph = wrapper.find('p.paragraph-node')
|
||||
expect(paragraph.exists()).toBe(true)
|
||||
expect(paragraph.find('figure').exists()).toBe(false)
|
||||
expect(paragraph.findAll('img')).toHaveLength(1)
|
||||
const img = paragraph.find('img')
|
||||
expect(img.exists()).toBe(true)
|
||||
expect(img.attributes('src')).toBe('https://example.com/vue.png')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'image links in same paragraph stay inline',
|
||||
markdown: `[](https://www.npmjs.com/package/markstream-vue)
|
||||
[](https://markstream-vue.simonhe.me/)
|
||||
[](https://github.com/Simon-He95/markstream-vue/actions/workflows/ci.yml)`,
|
||||
mountOptions: {
|
||||
global: {
|
||||
stubs: {
|
||||
'transition': false,
|
||||
'transition-group': false,
|
||||
},
|
||||
},
|
||||
},
|
||||
skipSnapshot: true,
|
||||
assert: (wrapper) => {
|
||||
const paragraph = wrapper.find('p.paragraph-node')
|
||||
expect(paragraph.exists()).toBe(true)
|
||||
expect(paragraph.element.tagName).toBe('P')
|
||||
|
||||
const childTagNames = Array.from(paragraph.element.children).map(child => child.tagName)
|
||||
expect(childTagNames).toEqual(['A', 'A', 'A'])
|
||||
|
||||
const links = paragraph.findAll('a.link-node')
|
||||
expect(links).toHaveLength(3)
|
||||
|
||||
const images = paragraph.findAll('img')
|
||||
expect(images).toHaveLength(3)
|
||||
expect(paragraph.findAll('figure')).toHaveLength(0)
|
||||
// Each link should contain an img (possibly wrapped in a container span)
|
||||
const linksWithImages = links.filter(link => link.find('img').exists())
|
||||
expect(linksWithImages).toHaveLength(3)
|
||||
expect(images[0].attributes('src')).toContain('img.shields.io/npm/v/markstream-vue')
|
||||
expect(images[1].attributes('src')).toContain('img.shields.io/badge/playground-live-34c759')
|
||||
expect(images[2].attributes('src')).toContain('/badge.svg')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'image links with relative target stay inline',
|
||||
markdown: `[](https://www.npmjs.com/package/markstream-vue)
|
||||
[](README.zh-CN.md)`,
|
||||
mountOptions: {
|
||||
global: {
|
||||
stubs: {
|
||||
'transition': false,
|
||||
'transition-group': false,
|
||||
},
|
||||
},
|
||||
},
|
||||
assert: (wrapper) => {
|
||||
const paragraph = wrapper.find('p.paragraph-node')
|
||||
expect(paragraph.exists()).toBe(true)
|
||||
expect(paragraph.element.tagName).toBe('P')
|
||||
|
||||
const childTagNames = Array.from(paragraph.element.children).map(child => child.tagName)
|
||||
expect(childTagNames).toEqual(['A', 'A'])
|
||||
|
||||
const links = paragraph.findAll('a.link-node')
|
||||
expect(links).toHaveLength(2)
|
||||
expect(paragraph.findAll('span.text-node')).toHaveLength(0)
|
||||
|
||||
const images = paragraph.findAll('img')
|
||||
expect(images).toHaveLength(2)
|
||||
expect(links[0].find('img').exists()).toBe(true)
|
||||
expect(links[1].find('img').exists()).toBe(true)
|
||||
expect(links[1].attributes('href')).toBe('README.zh-CN.md')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'inline code node',
|
||||
markdown: 'Here is `const answer = 42` inline.',
|
||||
expectedText: ['Here is', 'const answer = 42', 'inline.'],
|
||||
assert: (wrapper) => {
|
||||
const code = wrapper.find('code')
|
||||
|
||||
expect(code.exists()).toBe(true)
|
||||
expect(code.text()).toBe('const answer = 42')
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'code block node',
|
||||
markdown: '```ts\nexport const sum = (a: number, b: number) => a + b\n```',
|
||||
props: { renderCodeBlocksAsPre: true },
|
||||
expectedText: ['export const sum = (a: number, b: number) => a + b'],
|
||||
assert: async (wrapper) => {
|
||||
await flushAll()
|
||||
const fallback = wrapper.find('pre code')
|
||||
expect(fallback.exists()).toBe(true)
|
||||
expect(fallback.text()).toContain('export const sum = (a: number, b: number) => a + b')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mermaid block node',
|
||||
markdown: '```mermaid\ngraph LR;A-->B;\n```',
|
||||
skipSnapshot: true,
|
||||
assert: async (wrapper) => {
|
||||
await flushAll()
|
||||
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||
|
||||
const mermaid = wrapper.find('._mermaid')
|
||||
expect(mermaid.exists()).toBe(true)
|
||||
// do not rely on exact toolbar SVG markup in snapshots (icons may
|
||||
// render differently in different envs); just assert the wrapper
|
||||
// contains the mermaid container
|
||||
expect(mermaid.html()).toContain('_mermaid')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'table node',
|
||||
markdown: '| Name | Role |\n| --- | --- |\n| Alice | Developer |',
|
||||
expectedText: ['Name', 'Role', 'Alice', 'Developer'],
|
||||
assert: (wrapper) => {
|
||||
const table = wrapper.find('table')
|
||||
expect(table.exists()).toBe(true)
|
||||
expect(table.findAll('tbody tr')).toHaveLength(1)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'definition list node',
|
||||
markdown: '',
|
||||
props: {
|
||||
nodes: [
|
||||
{
|
||||
type: 'definition_list',
|
||||
raw: 'Term: Definition details',
|
||||
items: [
|
||||
{
|
||||
type: 'definition_item',
|
||||
raw: 'Term: Definition details',
|
||||
term: [
|
||||
{
|
||||
type: 'text',
|
||||
content: 'Term',
|
||||
raw: 'Term',
|
||||
},
|
||||
],
|
||||
definition: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
raw: 'Definition details',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
content: 'Definition details',
|
||||
raw: 'Definition details',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedText: ['Term', 'Definition details'],
|
||||
assert: (wrapper) => {
|
||||
const dl = wrapper.find('dl')
|
||||
expect(dl.exists()).toBe(true)
|
||||
expect(dl.findAll('dt')).toHaveLength(1)
|
||||
expect(dl.findAll('dd')).toHaveLength(1)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'footnote nodes',
|
||||
markdown: 'A footnote reference[^1].\n\n[^1]: Footnote explanation',
|
||||
expectedText: ['A footnote reference', 'Footnote explanation'],
|
||||
assert: (wrapper) => {
|
||||
const footnoteBlock = wrapper.find('[id="fnref-1"]')
|
||||
expect(footnoteBlock.exists()).toBe(true)
|
||||
expect(footnoteBlock.text()).toBe('[1]')
|
||||
const footerAnchor = wrapper.find('.footnote-anchor')
|
||||
expect(footerAnchor.exists()).toBe(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'footnote reference node',
|
||||
markdown: '',
|
||||
props: {
|
||||
nodes: [
|
||||
{
|
||||
type: 'footnote_reference',
|
||||
id: '1',
|
||||
raw: '[^1]',
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedText: [],
|
||||
assert: (wrapper) => {
|
||||
const reference = wrapper.find('sup.footnote-reference')
|
||||
expect(reference.exists()).toBe(true)
|
||||
expect(reference.text()).toContain('1')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'admonition node',
|
||||
markdown: '::: warning\nAdmonition content\n:::',
|
||||
expectedText: 'Admonition content',
|
||||
assert: (wrapper) => {
|
||||
const admonition = wrapper.find('.admonition-warning')
|
||||
expect(admonition.exists()).toBe(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'thematic break node',
|
||||
markdown: 'First section.\n\n---\n\nSecond section.',
|
||||
expectedText: ['First section.', 'Second section.'],
|
||||
assert: (wrapper) => {
|
||||
const hr = wrapper.find('hr')
|
||||
expect(hr.exists()).toBe(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'hardbreak node',
|
||||
markdown: 'Line one \nLine two',
|
||||
expectedText: ['Line one', 'Line two'],
|
||||
assert: (wrapper) => {
|
||||
expect(wrapper.findAll('br.hard-break').length).toBeGreaterThan(0)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'strong node',
|
||||
markdown: 'This is **bold text**.',
|
||||
expectedText: ['This is', 'bold text'],
|
||||
assert: (wrapper) => {
|
||||
const strong = wrapper.find('strong')
|
||||
expect(strong.exists()).toBe(true)
|
||||
expect(strong.text()).toBe('bold text')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'emphasis node',
|
||||
markdown: 'This is *italic text*.',
|
||||
expectedText: ['This is', 'italic text'],
|
||||
assert: (wrapper) => {
|
||||
const em = wrapper.find('em')
|
||||
expect(em.exists()).toBe(true)
|
||||
expect(em.text()).toBe('italic text')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'strikethrough node',
|
||||
markdown: 'This is ~~deleted text~~.',
|
||||
expectedText: ['This is', 'deleted text'],
|
||||
assert: (wrapper) => {
|
||||
const del = wrapper.find('s, del')
|
||||
expect(del.exists()).toBe(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'highlight node',
|
||||
markdown: 'Use ==highlighted text== for emphasis.',
|
||||
expectedText: ['Use', 'highlighted text', 'for emphasis.'],
|
||||
assert: (wrapper) => {
|
||||
const mark = wrapper.find('mark.highlight-node')
|
||||
expect(mark.exists()).toBe(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'insert node',
|
||||
markdown: 'Here comes ++inserted text++ in the paragraph.',
|
||||
expectedText: ['Here comes', 'inserted text', 'in the paragraph.'],
|
||||
assert: (wrapper) => {
|
||||
const ins = wrapper.find('ins.insert-node')
|
||||
expect(ins.exists()).toBe(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'subscript node',
|
||||
markdown: 'Chemical formula H~2~O is familiar.',
|
||||
expectedText: ['Chemical formula', 'H2O', 'is familiar.'],
|
||||
assert: (wrapper) => {
|
||||
const sub = wrapper.find('sub')
|
||||
expect(sub.exists()).toBe(true)
|
||||
expect(sub.text()).toBe('2')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'superscript node',
|
||||
markdown: 'Math uses x^2^ frequently.',
|
||||
expectedText: ['Math uses', 'x2', 'frequently.'],
|
||||
assert: (wrapper) => {
|
||||
const sup = wrapper.find('sup')
|
||||
expect(sup.exists()).toBe(true)
|
||||
expect(sup.text()).toBe('2')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'emoji node',
|
||||
markdown: 'Smile with :smile: emoji.',
|
||||
expectedText: ['Smile with', 'emoji.'],
|
||||
assert: (wrapper) => {
|
||||
const emoji = wrapper.find('span.emoji-node')
|
||||
expect(emoji.exists()).toBe(true)
|
||||
expect(emoji.text()).toBe('😄')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'math inline node',
|
||||
markdown: 'Einstein wrote $E=mc^2$.',
|
||||
skipSnapshot: false,
|
||||
// Accept either raw Dollar-delimited text or rendered KaTeX output
|
||||
expectedText: ['Einstein wrote', 'E=mc^2'],
|
||||
assert: async (wrapper) => {
|
||||
await flushAll()
|
||||
await flushAll()
|
||||
const inline = wrapper.find('.math-inline')
|
||||
if (inline.exists())
|
||||
expect(wrapper.html()).toContain('katex')
|
||||
else
|
||||
expect(wrapper.text()).toContain('Einstein wrote $E=mc^2$.')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'math block node',
|
||||
markdown: '$$\na^2 + b^2 = c^2\n$$',
|
||||
skipSnapshot: true,
|
||||
// do not assert expectedText globally because math rendering is
|
||||
// optional and may be handled asynchronously or by KaTeX which
|
||||
// isn't guaranteed in the test env
|
||||
expectedText: [],
|
||||
assert: (wrapper) => {
|
||||
const block = wrapper.find('.math-block')
|
||||
// math rendering is optional (requires katex). Accept either
|
||||
// a rendered KaTeX block or the raw LaTeX fallback
|
||||
if (block.exists()) {
|
||||
// prefer KaTeX output, but tolerate absence
|
||||
const hasKaTeX = wrapper.html().includes('katex-display') || wrapper.html().includes('katex')
|
||||
if (!hasKaTeX) {
|
||||
expect(wrapper.text()).toContain('a^2 + b^2 = c^2')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'reference node',
|
||||
markdown: 'Cite research [1] for details.',
|
||||
expectedText: ['Cite research', 'for details.'],
|
||||
assert: (wrapper) => {
|
||||
const reference = wrapper.find('span.reference-node')
|
||||
expect(reference.exists()).toBe(true)
|
||||
expect(reference.text()).toBe('1')
|
||||
expect(reference.classes()).toContain('reference-node')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'complex inline code with emphasis and multiple code spans',
|
||||
markdown: '1. **整合冗余条款:** 将原`1-(5)`、`3-(3)`、`3-(4)`中关于“缴纳费用”和“告知法律状态变化”的核心义务合并,统一放入`1-(4)`“法律状态维护与通知义务”中,使其逻辑更集中,避免了在不同部分重复提及相似义务。',
|
||||
expectedText: ['整合冗余条款', '1-(5)', '3-(3)', '3-(4)', '法律状态维护与通知义务'],
|
||||
// assert removed: rely on expectedText assertions to validate parsing
|
||||
},
|
||||
]
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
it(`renders ${scenario.name}`, async () => {
|
||||
const wrapper = await mountMarkdown(scenario.markdown, scenario.props, scenario.mountOptions)
|
||||
try {
|
||||
if (scenario.assert)
|
||||
await scenario.assert(wrapper)
|
||||
|
||||
const textContent = normalizeText(wrapper.text())
|
||||
if (Array.isArray(scenario.expectedText)) {
|
||||
for (const snippet of scenario.expectedText)
|
||||
expect(textContent).toContain(snippet)
|
||||
}
|
||||
else if (typeof scenario.expectedText === 'string' && scenario.expectedText.length > 0) {
|
||||
expect(textContent).toContain(scenario.expectedText)
|
||||
}
|
||||
|
||||
if (!scenario.skipSnapshot) {
|
||||
const snapshotHtml = sanitizeSnapshotHtml(wrapper.html(), scenario.name)
|
||||
expect(snapshotHtml).toMatchSnapshot()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
}, 20000)
|
||||
}
|
||||
|
||||
it('resizes table columns from header drag handles', async () => {
|
||||
const wrapper = await mountMarkdown('| Name | Role |\n| --- | --- |\n| Alice | Developer |')
|
||||
try {
|
||||
const headers = wrapper.findAll('thead th')
|
||||
expect(headers).toHaveLength(2)
|
||||
|
||||
const headerWidths = [120, 240]
|
||||
headers.forEach((header, index) => {
|
||||
Object.defineProperty(header.element, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => ({
|
||||
bottom: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: headerWidths[index],
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
const handle = headers[0].find('.table-node__resize-handle')
|
||||
expect(handle.exists()).toBe(true)
|
||||
|
||||
handle.element.dispatchEvent(new MouseEvent('pointerdown', {
|
||||
bubbles: true,
|
||||
button: 0,
|
||||
clientX: 120,
|
||||
}))
|
||||
window.dispatchEvent(new MouseEvent('pointermove', {
|
||||
bubbles: true,
|
||||
clientX: 180,
|
||||
}))
|
||||
window.dispatchEvent(new MouseEvent('pointerup', { bubbles: true }))
|
||||
await flushAll()
|
||||
|
||||
const columns = wrapper.findAll('col')
|
||||
expect(columns).toHaveLength(2)
|
||||
expect(columns[0].attributes('style')).toContain('width: 180px')
|
||||
expect(columns[1].attributes('style')).toContain('width: 180px')
|
||||
}
|
||||
finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('allows overriding `list_item` via customComponents', async () => {
|
||||
const scopeId = 'custom-components-list-item'
|
||||
const CustomListItem = defineComponent({
|
||||
name: 'CustomListItem',
|
||||
props: {
|
||||
node: { type: Object, required: true },
|
||||
indexKey: [String, Number],
|
||||
value: Number,
|
||||
customId: String,
|
||||
typewriter: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
return () => h('li', { 'class': 'custom-list-item', 'data-value': props.value == null ? '' : String(props.value) }, [
|
||||
h(MarkdownRender, {
|
||||
nodes: (props.node as any).children || [],
|
||||
customId: props.customId,
|
||||
indexKey: `custom-list-item-${String(props.indexKey ?? '')}`,
|
||||
typewriter: props.typewriter,
|
||||
batchRendering: false,
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
setCustomComponents(scopeId, { list_item: CustomListItem })
|
||||
|
||||
try {
|
||||
const wrapper = await mountMarkdown('1. First entry\n2. Second entry', { customId: scopeId, final: true })
|
||||
try {
|
||||
const items = wrapper.findAll('li.custom-list-item')
|
||||
expect(items).toHaveLength(2)
|
||||
expect(items[0].attributes('data-value')).toBe('1')
|
||||
expect(items[1].attributes('data-value')).toBe('2')
|
||||
expect(normalizeText(wrapper.text())).toMatch(/First entry\s*Second entry/)
|
||||
}
|
||||
finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
removeCustomComponents(scopeId)
|
||||
}
|
||||
})
|
||||
|
||||
it('replays a fade animation when a streamed non-code node updates in place', async () => {
|
||||
const wrapper = await mountMarkdown('Hello')
|
||||
try {
|
||||
const nodeContent = () => wrapper.find('[data-node-index="0"] .node-content')
|
||||
expect(nodeContent().attributes('style') ?? '').toBe('')
|
||||
expect(wrapper.find('.text-node-stream-delta').exists()).toBe(false)
|
||||
|
||||
await wrapper.setProps({ content: 'Hello world' })
|
||||
await flushAll()
|
||||
|
||||
expect(nodeContent().attributes('style') ?? '').toBe('')
|
||||
const firstDelta = wrapper.find('.text-node-stream-delta')
|
||||
expect(firstDelta.exists()).toBe(true)
|
||||
expect(firstDelta.text()).toBe('world')
|
||||
expect(normalizeText(wrapper.text())).toContain('Hello world')
|
||||
|
||||
await wrapper.setProps({ content: 'Hello world again' })
|
||||
await flushAll()
|
||||
|
||||
expect(nodeContent().attributes('style') ?? '').toBe('')
|
||||
const secondDelta = wrapper.find('.text-node-stream-delta')
|
||||
expect(secondDelta.exists()).toBe(true)
|
||||
expect(secondDelta.text()).toBe('again')
|
||||
expect(normalizeText(wrapper.text())).toContain('Hello world again')
|
||||
}
|
||||
finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('renders repeated custom components without slot content reuse', async () => {
|
||||
const scopeId = 'custom-components-repeat'
|
||||
const NewQuestion = defineComponent({
|
||||
name: 'NewQuestion',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'new-question' }, slots.default?.())
|
||||
},
|
||||
})
|
||||
const SubQuestion = defineComponent({
|
||||
name: 'SubQuestion',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'sub-question' }, slots.default?.())
|
||||
},
|
||||
})
|
||||
|
||||
setCustomComponents(scopeId, {
|
||||
'new-question': NewQuestion,
|
||||
'sub-question': SubQuestion,
|
||||
})
|
||||
|
||||
try {
|
||||
const markdown = `<new-question>A</new-question>
|
||||
<sub-question>
|
||||
B
|
||||
</sub-question>
|
||||
|
||||
<sub-question>
|
||||
C
|
||||
</sub-question>`
|
||||
const wrapper = await mountMarkdown(markdown, {
|
||||
customId: scopeId,
|
||||
final: true,
|
||||
})
|
||||
try {
|
||||
expect(normalizeText(wrapper.text())).toContain('A B C')
|
||||
}
|
||||
finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
removeCustomComponents(scopeId)
|
||||
}
|
||||
})
|
||||
|
||||
it('renders nested repeated custom components without content bleed', async () => {
|
||||
const scopeId = 'custom-components-nested-repeat'
|
||||
const NewQuestion = defineComponent({
|
||||
name: 'NewQuestion',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'new-question' }, slots.default?.())
|
||||
},
|
||||
})
|
||||
const SubQuestion = defineComponent({
|
||||
name: 'SubQuestion',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'sub-question' }, slots.default?.())
|
||||
},
|
||||
})
|
||||
|
||||
setCustomComponents(scopeId, {
|
||||
'new-question': NewQuestion,
|
||||
'sub-question': SubQuestion,
|
||||
})
|
||||
|
||||
try {
|
||||
const markdown = `<sub-question>
|
||||
<new-question>A</new-question>
|
||||
<sub-question>Inner-1</sub-question>
|
||||
</sub-question>
|
||||
|
||||
<sub-question>
|
||||
<new-question>B</new-question>
|
||||
<sub-question>Inner-2</sub-question>
|
||||
</sub-question>`
|
||||
const wrapper = await mountMarkdown(markdown, { customId: scopeId, final: true })
|
||||
try {
|
||||
expect(normalizeText(wrapper.text())).toMatch(/A\s*Inner-1\s*B\s*Inner-2/)
|
||||
}
|
||||
finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
removeCustomComponents(scopeId)
|
||||
}
|
||||
})
|
||||
|
||||
it('renders repeated custom components with different attrs and explicit keys', async () => {
|
||||
const scopeId = 'custom-components-attrs-repeat'
|
||||
const SubQuestion = defineComponent({
|
||||
name: 'SubQuestion',
|
||||
setup(_, { slots, attrs }) {
|
||||
const id = attrs['data-id'] == null ? '' : String(attrs['data-id'])
|
||||
return () => h('div', { class: 'sub-question' }, [
|
||||
h('span', { class: 'sub-question__id' }, id),
|
||||
slots.default?.(),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
setCustomComponents(scopeId, {
|
||||
'sub-question': SubQuestion,
|
||||
})
|
||||
|
||||
try {
|
||||
const markdown = `<sub-question key="k1" data-id="1">B</sub-question>
|
||||
<sub-question key="k2" data-id="2">C</sub-question>`
|
||||
const wrapper = await mountMarkdown(markdown, { customId: scopeId, final: true })
|
||||
try {
|
||||
expect(normalizeText(wrapper.text())).toMatch(/1\s*B\s*2\s*C/)
|
||||
}
|
||||
finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
removeCustomComponents(scopeId)
|
||||
}
|
||||
})
|
||||
|
||||
it('renders custom components interleaved with markdown blocks', async () => {
|
||||
const scopeId = 'custom-components-mixed-markdown'
|
||||
const NewQuestion = defineComponent({
|
||||
name: 'NewQuestion',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'new-question' }, slots.default?.())
|
||||
},
|
||||
})
|
||||
const SubQuestion = defineComponent({
|
||||
name: 'SubQuestion',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'sub-question' }, slots.default?.())
|
||||
},
|
||||
})
|
||||
|
||||
setCustomComponents(scopeId, {
|
||||
'new-question': NewQuestion,
|
||||
'sub-question': SubQuestion,
|
||||
})
|
||||
|
||||
try {
|
||||
const markdown = `<div><new-question>A</new-question></div>
|
||||
|
||||
Normal paragraph between.
|
||||
|
||||
- item-1
|
||||
- item-2
|
||||
|
||||
<div><sub-question>B</sub-question></div>
|
||||
|
||||
Another paragraph.
|
||||
|
||||
<div><sub-question>C</sub-question></div>`
|
||||
const wrapper = await mountMarkdown(markdown, { customId: scopeId, final: true })
|
||||
try {
|
||||
const text = normalizeText(wrapper.text())
|
||||
expect(text).toContain('A')
|
||||
expect(text).toContain('Normal paragraph between.')
|
||||
expect(text).toMatch(/item-1\s*item-2/)
|
||||
expect(text).toMatch(/A[^B]*B[\s\S]*C/)
|
||||
}
|
||||
finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
removeCustomComponents(scopeId)
|
||||
}
|
||||
})
|
||||
|
||||
it('renders repeated custom components mixed with standard HTML inside one block', async () => {
|
||||
const scopeId = 'custom-components-mixed-html'
|
||||
const SubQuestion = defineComponent({
|
||||
name: 'SubQuestion',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'sub-question' }, slots.default?.())
|
||||
},
|
||||
})
|
||||
|
||||
setCustomComponents(scopeId, {
|
||||
'sub-question': SubQuestion,
|
||||
})
|
||||
|
||||
try {
|
||||
const markdown = `<div>
|
||||
<sub-question>First <strong>X</strong></sub-question>
|
||||
<span>middle</span>
|
||||
<sub-question>Second <em>Y</em></sub-question>
|
||||
</div>`
|
||||
const wrapper = await mountMarkdown(markdown, { customId: scopeId, final: true })
|
||||
try {
|
||||
expect(normalizeText(wrapper.text())).toMatch(/First\s*X\s*middle\s*Second\s*Y/)
|
||||
}
|
||||
finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
removeCustomComponents(scopeId)
|
||||
}
|
||||
})
|
||||
|
||||
it('passes node props to custom components when using nodes prop with customHtmlTags', async () => {
|
||||
const scopeId = 'custom-nodes-prop-thinking'
|
||||
const ThinkingNode = defineComponent({
|
||||
name: 'ThinkingNode',
|
||||
props: {
|
||||
node: { type: Object, default: () => ({ loading: false, content: '' }) },
|
||||
},
|
||||
setup(props) {
|
||||
return () => h('div', { class: 'thinking-node' }, props.node.content)
|
||||
},
|
||||
})
|
||||
|
||||
setCustomComponents(scopeId, {
|
||||
thinking: (props: any) => h(ThinkingNode, props),
|
||||
})
|
||||
|
||||
try {
|
||||
// Pre-parsed nodes as if parsed without customHtmlTags – the thinking
|
||||
// tag is still an html_block with tag:"thinking".
|
||||
const nodes = [
|
||||
{
|
||||
type: 'html_block',
|
||||
tag: 'thinking',
|
||||
content: '<thinking>I am thinking</thinking>',
|
||||
raw: '<thinking>I am thinking</thinking>',
|
||||
loading: false,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'text', content: 'Hello', raw: 'Hello' }],
|
||||
},
|
||||
]
|
||||
|
||||
const wrapper = mount(MarkdownRender, {
|
||||
props: {
|
||||
nodes,
|
||||
final: true,
|
||||
customId: scopeId,
|
||||
customHtmlTags: ['thinking'],
|
||||
},
|
||||
})
|
||||
await flushAll()
|
||||
|
||||
try {
|
||||
const text = normalizeText(wrapper.text())
|
||||
expect(text).toContain('I am thinking')
|
||||
expect(text).toContain('Hello')
|
||||
// The custom component should be rendered (not the default HtmlBlockNode)
|
||||
expect(wrapper.find('.thinking-node').exists()).toBe(true)
|
||||
}
|
||||
finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
removeCustomComponents(scopeId)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user