|
|
|
import * as assert$1 from 'assert';
|
|
|
|
import * as jsdom from 'jsdom';
|
|
|
|
import glob from 'tiny-glob/sync';
|
|
|
|
import * as path from 'path';
|
|
|
|
import * as fs from 'fs';
|
|
|
|
import * as colors from 'kleur';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {typeof assert$1 & { htmlEqual: (actual: string, expected: string, message?: string) => void, htmlEqualWithOptions: (actual: string, expected: string, options: { preserveComments?: boolean, withoutNormalizeHtml?: boolean }, message?: string) => void }}
|
|
|
|
*/
|
|
|
|
export const assert = /** @type {any} */ (assert$1);
|
|
|
|
|
|
|
|
// for coverage purposes, we need to test source files,
|
|
|
|
// but for sanity purposes, we need to test dist files
|
|
|
|
export function loadSvelte(test = false) {
|
|
|
|
process.env.TEST = test ? 'true' : '';
|
|
|
|
|
|
|
|
const resolved = require.resolve('../compiler.js');
|
|
|
|
|
|
|
|
delete require.cache[resolved];
|
|
|
|
return require(resolved);
|
|
|
|
}
|
|
|
|
|
|
|
|
export const svelte = loadSvelte();
|
|
|
|
|
|
|
|
export function exists(path) {
|
|
|
|
try {
|
|
|
|
fs.statSync(path);
|
|
|
|
return true;
|
|
|
|
} catch (err) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function tryToLoadJson(file) {
|
|
|
|
try {
|
|
|
|
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
|
|
} catch (err) {
|
|
|
|
if (err.code !== 'ENOENT') throw err;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function tryToReadFile(file) {
|
|
|
|
try {
|
|
|
|
return fs.readFileSync(file, 'utf-8');
|
|
|
|
} catch (err) {
|
|
|
|
if (err.code !== 'ENOENT') throw err;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function cleanRequireCache() {
|
|
|
|
Object.keys(require.cache)
|
|
|
|
.filter(x => x.endsWith('.svelte'))
|
|
|
|
.forEach(file => delete require.cache[file]);
|
|
|
|
}
|
|
|
|
|
|
|
|
const virtualConsole = new jsdom.VirtualConsole();
|
|
|
|
virtualConsole.sendTo(console);
|
|
|
|
|
|
|
|
const window = new jsdom.JSDOM('<main></main>', { virtualConsole }).window;
|
|
|
|
global.document = window.document;
|
|
|
|
global.navigator = window.navigator;
|
|
|
|
global.getComputedStyle = window.getComputedStyle;
|
|
|
|
global.requestAnimationFrame = null; // placeholder, filled in using set_raf
|
|
|
|
global.window = window;
|
|
|
|
|
|
|
|
// add missing ecmascript globals to window
|
|
|
|
for (const key of Object.getOwnPropertyNames(global)) {
|
|
|
|
if (!(key in window)) window[key] = global[key];
|
|
|
|
}
|
|
|
|
|
|
|
|
// implement mock scroll
|
|
|
|
window.scrollTo = function (pageXOffset, pageYOffset) {
|
|
|
|
window.pageXOffset = pageXOffset;
|
|
|
|
window.pageYOffset = pageYOffset;
|
|
|
|
};
|
|
|
|
|
|
|
|
export function env() {
|
|
|
|
window.document.title = '';
|
|
|
|
window.document.head.innerHTML = '';
|
|
|
|
window.document.body.innerHTML = '<main></main>';
|
|
|
|
|
|
|
|
return window;
|
|
|
|
}
|
|
|
|
|
|
|
|
function cleanChildren(node) {
|
|
|
|
let previous = null;
|
|
|
|
|
|
|
|
// sort attributes
|
|
|
|
const attributes = Array.from(node.attributes).sort((a, b) => {
|
|
|
|
return a.name < b.name ? -1 : 1;
|
|
|
|
});
|
|
|
|
|
|
|
|
attributes.forEach(attr => {
|
|
|
|
node.removeAttribute(attr.name);
|
|
|
|
});
|
|
|
|
|
|
|
|
attributes.forEach(attr => {
|
|
|
|
node.setAttribute(attr.name, attr.value);
|
|
|
|
});
|
|
|
|
|
|
|
|
// recurse
|
|
|
|
[...node.childNodes].forEach(child => {
|
|
|
|
if (child.nodeType === 3) {
|
|
|
|
// text
|
|
|
|
if (
|
|
|
|
node.namespaceURI === 'http://www.w3.org/2000/svg' &&
|
|
|
|
node.tagName !== 'text' &&
|
|
|
|
node.tagName !== 'tspan'
|
|
|
|
) {
|
|
|
|
node.removeChild(child);
|
|
|
|
}
|
|
|
|
|
|
|
|
child.data = child.data.replace(/[ \t\n\r\f]+/g, '\n');
|
|
|
|
|
|
|
|
if (previous && previous.nodeType === 3) {
|
|
|
|
previous.data += child.data;
|
|
|
|
previous.data = previous.data.replace(/[ \t\n\r\f]+/g, '\n');
|
|
|
|
|
|
|
|
node.removeChild(child);
|
|
|
|
child = previous;
|
|
|
|
}
|
|
|
|
} else if (child.nodeType === 8) {
|
|
|
|
// comment
|
|
|
|
// do nothing
|
|
|
|
} else {
|
|
|
|
cleanChildren(child);
|
|
|
|
}
|
|
|
|
|
|
|
|
previous = child;
|
|
|
|
});
|
|
|
|
|
|
|
|
// collapse whitespace
|
|
|
|
if (node.firstChild && node.firstChild.nodeType === 3) {
|
|
|
|
node.firstChild.data = node.firstChild.data.replace(/^[ \t\n\r\f]+/, '');
|
|
|
|
if (!node.firstChild.data.length) node.removeChild(node.firstChild);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.lastChild && node.lastChild.nodeType === 3) {
|
|
|
|
node.lastChild.data = node.lastChild.data.replace(/[ \t\n\r\f]+$/, '');
|
|
|
|
if (!node.lastChild.data.length) node.removeChild(node.lastChild);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {Window} window
|
|
|
|
* @param {string} html
|
|
|
|
* @param {{ removeDataSvelte?: boolean, preserveComments?: boolean }} param2
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
export function normalizeHtml(window, html, { removeDataSvelte = false, preserveComments = false }) {
|
|
|
|
try {
|
|
|
|
const node = window.document.createElement('div');
|
|
|
|
node.innerHTML = html
|
|
|
|
.replace(/(<!--.*?-->)/g, preserveComments ? '$1' : '')
|
|
|
|
.replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1')
|
|
|
|
.replace(/>[ \t\n\r\f]+</g, '><')
|
|
|
|
.trim();
|
|
|
|
cleanChildren(node);
|
|
|
|
return node.innerHTML.replace(/<\/?noscript\/?>/g, '');
|
|
|
|
} catch (err) {
|
|
|
|
throw new Error(`Failed to normalize HTML:\n${html}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} html
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
export function normalizeNewline(html) {
|
|
|
|
return html.replace(/\r\n/g, '\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {{ removeDataSvelte?: boolean }} options
|
|
|
|
*/
|
|
|
|
export function setupHtmlEqual(options = {}) {
|
|
|
|
const window = env();
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-import-assign
|
|
|
|
assert.htmlEqual = (actual, expected, message) => {
|
|
|
|
assert.deepEqual(
|
|
|
|
normalizeHtml(window, actual, options),
|
|
|
|
normalizeHtml(window, expected, options),
|
|
|
|
message
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {string} actual
|
|
|
|
* @param {string} expected
|
|
|
|
* @param {{ preserveComments?: boolean, withoutNormalizeHtml?: boolean }} param2
|
|
|
|
* @param {string?} message
|
|
|
|
*/
|
|
|
|
assert.htmlEqualWithOptions = (actual, expected, { preserveComments, withoutNormalizeHtml }, message) => {
|
|
|
|
assert.deepEqual(
|
|
|
|
withoutNormalizeHtml
|
|
|
|
? normalizeNewline(actual).replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
|
|
|
|
: normalizeHtml(window, actual, { ...options, preserveComments }),
|
|
|
|
withoutNormalizeHtml
|
|
|
|
? normalizeNewline(expected).replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
|
|
|
|
: normalizeHtml(window, expected, { ...options, preserveComments }),
|
|
|
|
message
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function loadConfig(file) {
|
|
|
|
try {
|
|
|
|
const resolved = require.resolve(file);
|
|
|
|
delete require.cache[resolved];
|
|
|
|
|
|
|
|
const config = require(resolved);
|
|
|
|
return config.default || config;
|
|
|
|
} catch (err) {
|
|
|
|
if (err.code === 'MODULE_NOT_FOUND') {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function addLineNumbers(code) {
|
|
|
|
return code
|
|
|
|
.split('\n')
|
|
|
|
.map((line, i) => {
|
|
|
|
i = String(i + 1);
|
|
|
|
while (i.length < 3) i = ` ${i}`;
|
|
|
|
|
|
|
|
return (
|
|
|
|
colors.gray(` ${i}: `) +
|
|
|
|
line.replace(/^\t+/, match => match.split('\t').join(' '))
|
|
|
|
);
|
|
|
|
})
|
|
|
|
.join('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
export function showOutput(cwd, options = {}, compile = svelte.compile) {
|
|
|
|
glob('**/*.svelte', { cwd }).forEach(file => {
|
|
|
|
if (file[0] === '_') return;
|
|
|
|
|
|
|
|
try {
|
|
|
|
const { js } = compile(
|
|
|
|
fs.readFileSync(`${cwd}/${file}`, 'utf-8'),
|
|
|
|
Object.assign(options, {
|
|
|
|
filename: file
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
console.log( // eslint-disable-line no-console
|
|
|
|
`\n>> ${colors.cyan().bold(file)}\n${addLineNumbers(js.code)}\n<< ${colors.cyan().bold(file)}`
|
|
|
|
);
|
|
|
|
} catch (err) {
|
|
|
|
console.log(`failed to generate output: ${err.message}`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export function shouldUpdateExpected() {
|
|
|
|
return process.argv.includes('--update');
|
|
|
|
}
|
|
|
|
|
|
|
|
export function spaces(i) {
|
|
|
|
let result = '';
|
|
|
|
while (i--) result += ' ';
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
// fake timers
|
|
|
|
const original_set_timeout = global.setTimeout;
|
|
|
|
|
|
|
|
export function useFakeTimers() {
|
|
|
|
const callbacks = [];
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
global.setTimeout = function (fn) {
|
|
|
|
callbacks.push(fn);
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
flush() {
|
|
|
|
callbacks.forEach(fn => fn());
|
|
|
|
callbacks.splice(0, callbacks.length);
|
|
|
|
},
|
|
|
|
removeFakeTimers() {
|
|
|
|
callbacks.splice(0, callbacks.length);
|
|
|
|
global.setTimeout = original_set_timeout;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function mkdirp(dir) {
|
|
|
|
const parent = path.dirname(dir);
|
|
|
|
if (parent === dir) return;
|
|
|
|
|
|
|
|
mkdirp(parent);
|
|
|
|
|
|
|
|
try {
|
|
|
|
fs.mkdirSync(dir);
|
|
|
|
} catch (err) {
|
|
|
|
// do nothing
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function prettyPrintPuppeteerAssertionError(message) {
|
|
|
|
const match = /Error: Expected "(.+)" to equal "(.+)"/.exec(message);
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
assert.equal(match[1], match[2]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {() => Promise<import ('puppeteer').Browser>} fn
|
|
|
|
* @param {number} maxAttempts
|
|
|
|
* @param {number} interval
|
|
|
|
* @returns {Promise<import ('puppeteer').Browser>}
|
|
|
|
*/
|
|
|
|
export async function retryAsync(fn, maxAttempts = 3, interval = 1000) {
|
|
|
|
let attempts = 0;
|
|
|
|
while (attempts <= maxAttempts) {
|
|
|
|
try {
|
|
|
|
return await fn();
|
|
|
|
} catch (err) {
|
|
|
|
if (++attempts >= maxAttempts) throw err;
|
|
|
|
await new Promise(resolve => setTimeout(resolve, interval));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* NOTE: Chromium may exit with SIGSEGV, so retry in that case
|
|
|
|
* @param {import ('puppeteer').Browser} browser
|
|
|
|
* @param {() => Promise<import ('puppeteer').Browser>} launchPuppeteer
|
|
|
|
* @param {() => void} additionalAssertion
|
|
|
|
* @param {(err: Error) => void} onError
|
|
|
|
* @returns {Promise<import ('puppeteer').Browser>}
|
|
|
|
*/
|
|
|
|
export async function executeBrowserTest(browser, launchPuppeteer, additionalAssertion, onError) {
|
|
|
|
let count = 0;
|
|
|
|
do {
|
|
|
|
count++;
|
|
|
|
try {
|
|
|
|
const page = await browser.newPage();
|
|
|
|
|
|
|
|
page.on('console', (type) => {
|
feat: custom elements rework (#8457)
This is an overhaul of custom elements in Svelte. Instead of compiling to a custom element class, the Svelte component class is mostly preserved as-is. Instead a wrapper is introduced which wraps a Svelte component constructor and returns a HTML element constructor. This has a couple of advantages:
- component can be used both as a custom element as well as a regular component. This allows creating one wrapper custom element and using regular Svelte components inside. Fixes #3594, fixes #3128, fixes #4274, fixes #5486, fixes #3422, fixes #2969, helps with https://github.com/sveltejs/kit/issues/4502
- all components are compiled with injected styles (inlined through Javascript), fixes #4274
- the wrapper instantiates the component in `connectedCallback` and disconnects it in `disconnectedCallback` (but only after one tick, because this could be a element move). Mount/destroy works as expected inside, fixes #5989, fixes #8191
- the wrapper forwards `addEventListener` calls to `component.$on`, which allows to listen to custom events, fixes #3119, closes #4142
- some things are hard to auto-configure, like attribute hyphen preferences or whether or not setting a property should reflect back to the attribute. This is why `<svelte:options customElement={..}>` can also take an object to modify such aspects. This option allows to specify whether setting a prop should be reflected back to the attribute (default `false`), what to use when converting the property to the attribute value and vice versa (through `type`, default `String`, or when `export let prop = false` then `Boolean`), and what the corresponding attribute for the property is (`attribute`, default lowercased prop name). These options are heavily inspired by lit: https://lit.dev/docs/components/properties. Closes #7638, fixes #5705
- adds a `shadowdom` option to control whether or not encapsulate the custom element. Closes #4330, closes #1748
Breaking changes:
- Wrapped Svelte component now stays as a regular Svelte component (invokeing it like before with `new Component({ target: ..})` won't create a custom element). Its custom element constructor is now a static property named `element` on the class (`Component.element`) and should be regularly invoked through setting it in the html.
- The timing of mount/destroy/update is different. Mount/destroy/updating a prop all happen after a tick, so `shadowRoot.innerHTML` won't immediately reflect the change (Lit does this too). If you rely on it, you need to await a promise
1 year ago
|
|
|
console[type.type()](type.text());
|
|
|
|
});
|
|
|
|
|
|
|
|
page.on('error', error => {
|
|
|
|
console.log('>>> an error happened');
|
|
|
|
console.error(error);
|
|
|
|
});
|
|
|
|
await page.goto('http://localhost:6789');
|
|
|
|
const result = await page.evaluate(() => {
|
|
|
|
// @ts-ignore -- It runs in browser context.
|
|
|
|
return test(document.querySelector('main'));
|
|
|
|
});
|
|
|
|
if (result) console.log(result);
|
|
|
|
additionalAssertion();
|
|
|
|
await page.close();
|
|
|
|
break;
|
|
|
|
} catch (err) {
|
|
|
|
if (count === 5 || browser.isConnected()) {
|
|
|
|
onError(err);
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
console.debug(err.stack || err);
|
|
|
|
console.log('RESTARTING Chromium...');
|
|
|
|
browser = await launchPuppeteer();
|
|
|
|
}
|
|
|
|
} while (count <= 5);
|
|
|
|
return browser;
|
|
|
|
}
|