mirror of https://github.com/sveltejs/svelte
parent
bd70ab621c
commit
49cff549c7
@ -0,0 +1,256 @@
|
|||||||
|
/**
|
||||||
|
* An object-based renderer for testing Svelte's custom renderer functionality.
|
||||||
|
*
|
||||||
|
* Runs in plain Node.js — no DOM required. Creates a tree of plain objects
|
||||||
|
* with simple properties (type, name, children, attributes, value, etc).
|
||||||
|
* Proves that Svelte can render into non-DOM targets.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createRenderer } from '../../src/renderer/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{ type: 'element', name: string, attributes: Record<string, string>, children: ObjNode[], listeners: Record<string, Array<{handler: any, options?: any}>>, parent: ObjNode | null, next_sibling: ObjNode | null, prev_sibling: ObjNode | null }} ObjElement
|
||||||
|
* @typedef {{ type: 'text', value: string, parent: ObjNode | null, next_sibling: ObjNode | null, prev_sibling: ObjNode | null }} ObjText
|
||||||
|
* @typedef {{ type: 'comment', value: string, parent: ObjNode | null, next_sibling: ObjNode | null, prev_sibling: ObjNode | null }} ObjComment
|
||||||
|
* @typedef {{ type: 'fragment', children: ObjNode[], parent: ObjNode | null, next_sibling: ObjNode | null, prev_sibling: ObjNode | null }} ObjFragment
|
||||||
|
* @typedef {ObjElement | ObjText | ObjComment | ObjFragment} ObjNode
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ObjNode & { children?: ObjNode[] }} parent
|
||||||
|
* @param {ObjNode} node
|
||||||
|
* @param {ObjNode | null} anchor
|
||||||
|
*/
|
||||||
|
function insert_node(parent, node, anchor) {
|
||||||
|
if (node.type === 'fragment') {
|
||||||
|
const children = [...(node.children ?? [])];
|
||||||
|
for (const child of children) {
|
||||||
|
insert_node(parent, child, anchor);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from old parent first
|
||||||
|
if (node.parent) {
|
||||||
|
remove_from_parent(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = /** @type {ObjNode[]} */ (parent.children);
|
||||||
|
node.parent = parent;
|
||||||
|
|
||||||
|
if (anchor === null) {
|
||||||
|
const last = children[children.length - 1] ?? null;
|
||||||
|
if (last) {
|
||||||
|
last.next_sibling = node;
|
||||||
|
node.prev_sibling = last;
|
||||||
|
}
|
||||||
|
node.next_sibling = null;
|
||||||
|
children.push(node);
|
||||||
|
} else {
|
||||||
|
const idx = children.indexOf(anchor);
|
||||||
|
if (idx === -1) throw new Error('Anchor not found in parent');
|
||||||
|
|
||||||
|
const prev = children[idx - 1] ?? null;
|
||||||
|
if (prev) {
|
||||||
|
prev.next_sibling = node;
|
||||||
|
node.prev_sibling = prev;
|
||||||
|
}
|
||||||
|
node.next_sibling = anchor;
|
||||||
|
anchor.prev_sibling = node;
|
||||||
|
children.splice(idx, 0, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {ObjNode} node */
|
||||||
|
function remove_from_parent(node) {
|
||||||
|
const parent = node.parent;
|
||||||
|
if (!parent || !('children' in parent)) return;
|
||||||
|
|
||||||
|
const children = parent.children;
|
||||||
|
const idx = children.indexOf(node);
|
||||||
|
if (idx === -1) return;
|
||||||
|
|
||||||
|
const prev = children[idx - 1] ?? null;
|
||||||
|
const next = children[idx + 1] ?? null;
|
||||||
|
if (prev) prev.next_sibling = next;
|
||||||
|
if (next) next.prev_sibling = prev;
|
||||||
|
|
||||||
|
node.prev_sibling = null;
|
||||||
|
node.next_sibling = null;
|
||||||
|
node.parent = null;
|
||||||
|
|
||||||
|
children.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = createRenderer({
|
||||||
|
createFragment() {
|
||||||
|
return /** @type {ObjFragment} */ ({
|
||||||
|
type: 'fragment',
|
||||||
|
children: [],
|
||||||
|
parent: null,
|
||||||
|
next_sibling: null,
|
||||||
|
prev_sibling: null
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createElement(name) {
|
||||||
|
return /** @type {ObjElement} */ ({
|
||||||
|
type: 'element',
|
||||||
|
name,
|
||||||
|
attributes: {},
|
||||||
|
children: [],
|
||||||
|
listeners: {},
|
||||||
|
parent: null,
|
||||||
|
next_sibling: null,
|
||||||
|
prev_sibling: null
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createTextNode(data) {
|
||||||
|
return /** @type {ObjText} */ ({
|
||||||
|
type: 'text',
|
||||||
|
value: data,
|
||||||
|
parent: null,
|
||||||
|
next_sibling: null,
|
||||||
|
prev_sibling: null
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createComment(data) {
|
||||||
|
return /** @type {ObjComment} */ ({
|
||||||
|
type: 'comment',
|
||||||
|
value: data,
|
||||||
|
parent: null,
|
||||||
|
next_sibling: null,
|
||||||
|
prev_sibling: null
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
nodeType(node) {
|
||||||
|
return node.type;
|
||||||
|
},
|
||||||
|
|
||||||
|
getNodeValue(node) {
|
||||||
|
if (node.type === 'text' || node.type === 'comment') return node.value;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getAttribute(element, name) {
|
||||||
|
return element.attributes?.[name] ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
setAttribute(element, key, value) {
|
||||||
|
element.attributes[key] = String(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAttribute(element, name) {
|
||||||
|
delete element.attributes[name];
|
||||||
|
},
|
||||||
|
|
||||||
|
hasAttribute(element, name) {
|
||||||
|
return name in (element.attributes ?? {});
|
||||||
|
},
|
||||||
|
|
||||||
|
setText(node, text) {
|
||||||
|
if (node.type === 'text' || node.type === 'comment') {
|
||||||
|
node.value = text;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getFirstChild(element) {
|
||||||
|
return element.children?.[0] ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getLastChild(element) {
|
||||||
|
const c = element.children;
|
||||||
|
return c?.[c.length - 1] ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getNextSibling(node) {
|
||||||
|
return node.next_sibling ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
insert(parent, element, anchor) {
|
||||||
|
insert_node(parent, element, anchor);
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(node) {
|
||||||
|
remove_from_parent(node);
|
||||||
|
},
|
||||||
|
|
||||||
|
getParent(node) {
|
||||||
|
return node.parent ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
addEventListener(target, type, handler, options) {
|
||||||
|
if (target.type !== 'element') return;
|
||||||
|
if (!target.listeners) target.listeners = {};
|
||||||
|
if (!target.listeners[type]) target.listeners[type] = [];
|
||||||
|
target.listeners[type].push({ handler, options });
|
||||||
|
},
|
||||||
|
|
||||||
|
removeEventListener(target, type, handler, options) {
|
||||||
|
if (target.type !== 'element') return;
|
||||||
|
if (!target.listeners?.[type]) return;
|
||||||
|
target.listeners[type] = target.listeners[type].filter(
|
||||||
|
(/** @type {any} */ l) => l.handler !== handler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- Test helpers ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a root object for mounting components into.
|
||||||
|
* @returns {ObjElement}
|
||||||
|
*/
|
||||||
|
export function create_root() {
|
||||||
|
return renderer.createElement('root');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch a synthetic event on an object node.
|
||||||
|
* @param {any} node
|
||||||
|
* @param {string} type
|
||||||
|
* @param {any} [detail]
|
||||||
|
*/
|
||||||
|
export function dispatch_event(node, type, detail) {
|
||||||
|
const listeners = node.listeners?.[type];
|
||||||
|
if (!listeners) return;
|
||||||
|
const event = { type, detail, target: node };
|
||||||
|
for (const { handler } of listeners) {
|
||||||
|
handler(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize an object tree to an HTML-like string for easy assertion.
|
||||||
|
* @param {ObjNode} node
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function serialize(node) {
|
||||||
|
if (!node) return '';
|
||||||
|
|
||||||
|
switch (node.type) {
|
||||||
|
case 'text':
|
||||||
|
return node.value ?? '';
|
||||||
|
case 'comment':
|
||||||
|
return '';
|
||||||
|
case 'fragment':
|
||||||
|
return node.children.map(serialize).join('');
|
||||||
|
case 'element': {
|
||||||
|
const tag = node.name;
|
||||||
|
let attrs = '';
|
||||||
|
const sorted_keys = Object.keys(node.attributes).sort();
|
||||||
|
for (const key of sorted_keys) {
|
||||||
|
attrs += ` ${key}="${node.attributes[key]}"`;
|
||||||
|
}
|
||||||
|
const children = node.children.map(serialize).join('');
|
||||||
|
return `<${tag}${attrs}>${children}</${tag}>`;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default renderer;
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
test({ assert, target, serialize }) {
|
||||||
|
const html = serialize(target);
|
||||||
|
assert.equal(
|
||||||
|
html,
|
||||||
|
'<root><div class="container" data-color="red"><span id="label">colored</span></div></root>'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify individual attribute access on the object node
|
||||||
|
const div = target.children.find(
|
||||||
|
(/** @type {any} */ n) => n.type === 'element' && n.name === 'div'
|
||||||
|
);
|
||||||
|
assert.ok(div);
|
||||||
|
assert.equal(div.attributes['class'], 'container');
|
||||||
|
assert.equal(div.attributes['data-color'], 'red');
|
||||||
|
|
||||||
|
const span = div.children.find(
|
||||||
|
(/** @type {any} */ n) => n.type === 'element' && n.name === 'span'
|
||||||
|
);
|
||||||
|
assert.ok(span);
|
||||||
|
assert.equal(span.attributes['id'], 'label');
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
let color = $state('red');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container" data-color={color}>
|
||||||
|
<span id="label">colored</span>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: '<p>hello</p>'
|
||||||
|
});
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<p>hello</p>
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: '<p>visible</p> <button>toggle</button>',
|
||||||
|
test({ assert, target, serialize, dispatch_event }) {
|
||||||
|
const button = target.children.find(
|
||||||
|
(/** @type {any} */ n) => n.type === 'element' && n.name === 'button'
|
||||||
|
);
|
||||||
|
assert.ok(button);
|
||||||
|
|
||||||
|
dispatch_event(button, 'click');
|
||||||
|
flushSync();
|
||||||
|
|
||||||
|
let html = serialize(target);
|
||||||
|
assert.equal(html, '<root><p>hidden</p> <button>toggle</button></root>');
|
||||||
|
|
||||||
|
dispatch_event(button, 'click');
|
||||||
|
flushSync();
|
||||||
|
|
||||||
|
html = serialize(target);
|
||||||
|
assert.equal(html, '<root><p>visible</p> <button>toggle</button></root>');
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
let visible = $state(true);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<p>visible</p>
|
||||||
|
{:else}
|
||||||
|
<p>hidden</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button onclick={() => visible = !visible}>toggle</button>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: '<ul><li>a</li><li>b</li><li>c</li></ul>'
|
||||||
|
});
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
let items = $state(['a', 'b', 'c']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{#each items as item}
|
||||||
|
<li>{item}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: '<button>click me</button> <p>0</p>',
|
||||||
|
test({ assert, target, serialize, dispatch_event }) {
|
||||||
|
const button = target.children.find(
|
||||||
|
(/** @type {any} */ n) => n.type === 'element' && n.name === 'button'
|
||||||
|
);
|
||||||
|
assert.ok(button);
|
||||||
|
|
||||||
|
dispatch_event(button, 'click');
|
||||||
|
flushSync();
|
||||||
|
|
||||||
|
const html = serialize(target);
|
||||||
|
assert.equal(html, '<root><button>click me</button> <p>1</p></root>');
|
||||||
|
|
||||||
|
dispatch_event(button, 'click');
|
||||||
|
dispatch_event(button, 'click');
|
||||||
|
flushSync();
|
||||||
|
|
||||||
|
const html2 = serialize(target);
|
||||||
|
assert.equal(html2, '<root><button>click me</button> <p>3</p></root>');
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
<script>
|
||||||
|
let count = $state(0);
|
||||||
|
|
||||||
|
function increment() {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={increment}>click me</button>
|
||||||
|
<p>{count}</p>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
/** @type {{ message: string }} */
|
||||||
|
let { message } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span>{message}</span>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: '<div><span>hello from child</span></div>'
|
||||||
|
});
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
import Child from './Child.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Child message="hello from child" />
|
||||||
|
</div>
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: '<button>clicks: 0</button>',
|
||||||
|
test({ assert, target, serialize, dispatch_event }) {
|
||||||
|
const button = target.children.find(
|
||||||
|
(/** @type {any} */ n) => n.type === 'element' && n.name === 'button'
|
||||||
|
);
|
||||||
|
assert.ok(button);
|
||||||
|
|
||||||
|
dispatch_event(button, 'click');
|
||||||
|
flushSync();
|
||||||
|
|
||||||
|
const html = serialize(target);
|
||||||
|
assert.equal(html, '<root><button>clicks: 1</button></root>');
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
let count = $state(0);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => count++}>
|
||||||
|
clicks: {count}
|
||||||
|
</button>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: '<div><span>hello default</span></div>'
|
||||||
|
});
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
/** @type {{ label?: string }} */
|
||||||
|
let { label = 'default' } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet greeting(name)}
|
||||||
|
<span>hello {name}</span>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{@render greeting(label)}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: '<p>hello world</p>'
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
/** @type {{ name?: string }} */
|
||||||
|
let { name = 'world' } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>hello {name}</p>
|
||||||
@ -0,0 +1,188 @@
|
|||||||
|
import * as path from 'node:path';
|
||||||
|
import { setImmediate } from 'node:timers/promises';
|
||||||
|
import { assert } from 'vitest';
|
||||||
|
import { compile_directory } from '../helpers.js';
|
||||||
|
import { suite_with_variants, type BaseTest } from '../suite.js';
|
||||||
|
import type { CompileOptions } from '#compiler';
|
||||||
|
import renderer, { create_root, serialize, dispatch_event } from './renderer.js';
|
||||||
|
|
||||||
|
export interface CustomRendererTest extends BaseTest {
|
||||||
|
html?: string;
|
||||||
|
compileOptions?: Partial<CompileOptions>;
|
||||||
|
props?: Record<string, any>;
|
||||||
|
error?: string;
|
||||||
|
runtime_error?: string;
|
||||||
|
warnings?: string[];
|
||||||
|
test?: (args: {
|
||||||
|
assert: typeof import('vitest').assert;
|
||||||
|
target: any;
|
||||||
|
component: Record<string, any>;
|
||||||
|
mod: any;
|
||||||
|
logs: any[];
|
||||||
|
warnings: any[];
|
||||||
|
renderer: typeof renderer;
|
||||||
|
serialize: typeof serialize;
|
||||||
|
dispatch_event: typeof dispatch_event;
|
||||||
|
}) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
const console_log = console.log;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
const console_warn = console.warn;
|
||||||
|
|
||||||
|
const renderer_path = path.resolve(import.meta.dirname, 'renderer.js');
|
||||||
|
|
||||||
|
export function custom_renderer_suite() {
|
||||||
|
return suite_with_variants<CustomRendererTest, 'custom-renderer', CompileOptions>(
|
||||||
|
['custom-renderer'],
|
||||||
|
(_variant, _config) => {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
(config, cwd) => {
|
||||||
|
return common_setup(cwd, config);
|
||||||
|
},
|
||||||
|
async (config, cwd, _variant, common) => {
|
||||||
|
await run_test(cwd, config, common);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function common_setup(cwd: string, config: CustomRendererTest) {
|
||||||
|
const compile_options: CompileOptions = {
|
||||||
|
generate: 'client',
|
||||||
|
rootDir: cwd,
|
||||||
|
runes: true,
|
||||||
|
customRenderer: renderer_path,
|
||||||
|
...config.compileOptions
|
||||||
|
};
|
||||||
|
|
||||||
|
await compile_directory(cwd, 'client', compile_options);
|
||||||
|
|
||||||
|
return compile_options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run_test(cwd: string, config: CustomRendererTest, compile_options: CompileOptions) {
|
||||||
|
let unintended_error = false;
|
||||||
|
let logs: any[] = [];
|
||||||
|
let warnings: any[] = [];
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = config.test?.toString() ?? '';
|
||||||
|
let n = 0;
|
||||||
|
let i = 0;
|
||||||
|
while (i < str.length) {
|
||||||
|
if (str[i] === '(') n++;
|
||||||
|
if (str[i] === ')' && --n === 0) break;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str.slice(0, i).includes('logs')) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log = (...args) => {
|
||||||
|
logs.push(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str.slice(0, i).includes('warnings') || config.warnings) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn = (...args) => {
|
||||||
|
if (typeof args[0] === 'string' && args[0].startsWith('%c[svelte]')) {
|
||||||
|
let message = args[0];
|
||||||
|
message = message.slice(message.indexOf('%c', 2) + 2);
|
||||||
|
const lines = message.split('\n');
|
||||||
|
if (lines.at(-1)?.startsWith('https://svelte.dev/e/')) {
|
||||||
|
lines.pop();
|
||||||
|
}
|
||||||
|
message = lines.join('\n');
|
||||||
|
warnings.push(message);
|
||||||
|
} else {
|
||||||
|
warnings.push(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mod = await import(`${cwd}/_output/client/main.svelte.js`);
|
||||||
|
const target = create_root();
|
||||||
|
|
||||||
|
let unmount: (() => void) | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
unmount = renderer.render(mod.default, {
|
||||||
|
target,
|
||||||
|
props: config.props ?? {}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (config.error) {
|
||||||
|
assert.include((err as Error).message, config.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.error) {
|
||||||
|
unintended_error = true;
|
||||||
|
assert.fail('Expected a runtime error');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.html) {
|
||||||
|
const html = serialize(target);
|
||||||
|
assert.equal(html, `<root>${config.html}</root>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (config.test) {
|
||||||
|
await config.test({
|
||||||
|
assert,
|
||||||
|
target,
|
||||||
|
component: config.props ?? {},
|
||||||
|
mod,
|
||||||
|
logs,
|
||||||
|
warnings,
|
||||||
|
renderer: renderer,
|
||||||
|
serialize,
|
||||||
|
dispatch_event
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.runtime_error) {
|
||||||
|
unintended_error = true;
|
||||||
|
assert.fail('Expected a runtime error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
unmount?.();
|
||||||
|
|
||||||
|
if (config.warnings) {
|
||||||
|
assert.deepEqual(warnings, config.warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// After unmount the target should be empty (only comments remain, which serialize to '')
|
||||||
|
const remaining = serialize(target);
|
||||||
|
assert.equal(
|
||||||
|
remaining,
|
||||||
|
'<root></root>',
|
||||||
|
'Expected component to leave nothing behind after unmount'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (config.runtime_error) {
|
||||||
|
assert.include((err as Error).message, config.runtime_error);
|
||||||
|
} else if (config.error && !unintended_error) {
|
||||||
|
assert.include((err as Error).message, config.error);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await setImmediate();
|
||||||
|
console.log = console_log;
|
||||||
|
console.warn = console_warn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ok(value: any): asserts value {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Expected truthy value, got ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
// @vitest-environment node
|
||||||
|
import { custom_renderer_suite, ok } from './shared';
|
||||||
|
|
||||||
|
const { test, run } = custom_renderer_suite();
|
||||||
|
|
||||||
|
export { test, ok };
|
||||||
|
|
||||||
|
await run(__dirname);
|
||||||
Loading…
Reference in new issue