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('', { 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 = '';
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]+<')
.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} fn
* @param {number} maxAttempts
* @param {number} interval
* @returns {Promise}
*/
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} launchPuppeteer
* @param {() => void} additionalAssertion
* @param {(err: Error) => void} onError
* @returns {Promise}
*/
export async function executeBrowserTest(browser, launchPuppeteer, additionalAssertion, onError) {
let count = 0;
do {
count++;
try {
const page = await browser.newPage();
page.on('console', (type) => {
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;
}