From 3df9d8466f712be14c99542f6a696f96db087b4d Mon Sep 17 00:00:00 2001 From: gtmnayan <50981692+gtm-nayan@users.noreply.github.com> Date: Mon, 22 May 2023 19:31:27 +0545 Subject: [PATCH] chore: speed up test execution (#8598) - shard runtime tests for better use of Vite's test parallelization - merge custom element and other browser tests to run in one test suite and use esbuild in it - on some setups only generate code output when test fails --- .gitignore | 1 + .prettierignore | 1 + package.json | 1 + pnpm-lock.yaml | 3 + test/css/css.test.js | 4 +- test/custom-elements/assert.js | 50 --- test/custom-elements/custom-elements.test.js | 118 ------ test/helpers.js | 2 +- test/js/js-output.test.js | 2 +- test/parser/parser.test.js | 2 +- test/preprocess/preprocess.test.js | 4 +- test/runtime-browser/browser.test.js | 364 ++++++++++-------- .../$$props/main.svelte | 0 .../custom-elements-samples}/$$props/test.js | 0 .../$$slot-dynamic-content/main.svelte | 0 .../$$slot-dynamic-content/my-widget.svelte | 0 .../$$slot-dynamic-content/test.js | 0 .../$$slot/main.svelte | 0 .../custom-elements-samples}/$$slot/test.js | 0 .../action/main.svelte | 0 .../custom-elements-samples}/action/test.js | 0 .../camel-case-attribute/main.svelte | 0 .../camel-case-attribute/test.js | 0 .../ce-options-valid/main.svelte | 0 .../ce-options-valid/test.js | 0 .../custom-method/main.svelte | 0 .../custom-method/test.js | 0 .../escaped-css/main.svelte | 0 .../escaped-css/test.js | 0 .../events/main.svelte | 0 .../custom-elements-samples}/events/test.js | 0 .../extended-builtin/_config.js | 0 .../extended-builtin/custom-button.js | 0 .../extended-builtin/main.svelte | 0 .../extended-builtin/test.js | 0 .../html-slots/main.svelte | 0 .../html-slots/test.js | 0 .../custom-elements-samples}/html/main.svelte | 0 .../custom-elements-samples}/html/test.js | 0 .../nested/Counter.svelte | 0 .../nested/main.svelte | 0 .../custom-elements-samples}/nested/test.js | 0 .../new-styled/main.svelte | 0 .../new-styled/test.js | 0 .../no-missing-prop-warnings/_config.js | 0 .../no-missing-prop-warnings/main.svelte | 0 .../no-missing-prop-warnings/test.js | 0 .../no-shadow-dom/main.svelte | 0 .../no-shadow-dom/test.js | 0 .../no-tag/_config.js | 0 .../no-tag/main.svelte | 0 .../custom-elements-samples}/no-tag/test.js | 0 .../oncreate/main.svelte | 0 .../custom-elements-samples}/oncreate/test.js | 0 .../ondestroy/main.svelte | 0 .../ondestroy/test.js | 0 .../props/main.svelte | 0 .../props/my-widget.svelte | 0 .../custom-elements-samples}/props/test.js | 0 .../reflect-attributes/main.svelte | 0 .../reflect-attributes/my-widget.svelte | 0 .../reflect-attributes/test.js | 0 test/runtime/runtime.shared.js | 272 +++++++++++++ test/runtime/runtime.test.js | 292 -------------- test/runtime/runtime_base.test.js | 22 ++ test/server-side-rendering/ssr-2.test.js | 50 ++- test/sourcemaps/sourcemaps.test.js | 4 +- test/stats/stats.test.js | 2 +- test/validator/validator.test.js | 2 +- test/vars/vars.test.js | 2 +- test/vitest-global-setup.js | 27 ++ vitest.config.js | 10 +- 72 files changed, 579 insertions(+), 656 deletions(-) delete mode 100644 test/custom-elements/assert.js delete mode 100644 test/custom-elements/custom-elements.test.js rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/$$props/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/$$props/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/$$slot-dynamic-content/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/$$slot-dynamic-content/my-widget.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/$$slot-dynamic-content/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/$$slot/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/$$slot/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/action/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/action/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/camel-case-attribute/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/camel-case-attribute/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/ce-options-valid/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/ce-options-valid/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/custom-method/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/custom-method/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/escaped-css/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/escaped-css/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/events/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/events/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/extended-builtin/_config.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/extended-builtin/custom-button.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/extended-builtin/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/extended-builtin/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/html-slots/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/html-slots/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/html/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/html/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/nested/Counter.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/nested/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/nested/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/new-styled/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/new-styled/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/no-missing-prop-warnings/_config.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/no-missing-prop-warnings/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/no-missing-prop-warnings/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/no-shadow-dom/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/no-shadow-dom/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/no-tag/_config.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/no-tag/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/no-tag/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/oncreate/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/oncreate/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/ondestroy/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/ondestroy/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/props/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/props/my-widget.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/props/test.js (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/reflect-attributes/main.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/reflect-attributes/my-widget.svelte (100%) rename test/{custom-elements/samples => runtime-browser/custom-elements-samples}/reflect-attributes/test.js (100%) create mode 100644 test/runtime/runtime.shared.js delete mode 100644 test/runtime/runtime.test.js create mode 100644 test/runtime/runtime_base.test.js create mode 100644 test/vitest-global-setup.js diff --git a/.gitignore b/.gitignore index 2231de9065..963ae9f26e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ node_modules /animate /scratch/ /test/*/samples/_ +/test/runtime/shards /yarn-error.log _actual*.* _output diff --git a/.prettierignore b/.prettierignore index 15f58330d5..0b3348e52f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,3 +12,4 @@ src/compiler/compile/internal_exports.js /test/**/expected* /test/**/_output /types +!vitest.config.js diff --git a/package.json b/package.json index b66101ed2d..b94bebb49b 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "axobject-query": "^3.1.1", "code-red": "^1.0.0", "css-tree": "^2.3.1", + "esbuild": "^0.17.19", "eslint": "^8.40.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a22326634d..51ce202cdf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,6 +67,9 @@ devDependencies: css-tree: specifier: ^2.3.1 version: 2.3.1 + esbuild: + specifier: ^0.17.19 + version: 0.17.19 eslint: specifier: ^8.40.0 version: 8.40.0 diff --git a/test/css/css.test.js b/test/css/css.test.js index 54f3600f5a..eed4862e60 100644 --- a/test/css/css.test.js +++ b/test/css/css.test.js @@ -2,7 +2,7 @@ import * as fs from 'node:fs'; import { assert, describe, it } from 'vitest'; -import * as svelte from '../../src/compiler/index.js'; +import * as svelte from 'svelte/compiler'; import { create_loader, should_update_expected, try_load_config } from '../helpers.js'; import { assert_html_equal } from '../html_equal.js'; @@ -116,7 +116,7 @@ describe('css', () => { }); function replace_css_hash(str) { - return str.replace(/svelte(-ref)?-[a-z0-9]+/g, (m, $1) => ($1 ? m : 'svelte-xyz')); + return str.replace(/svelte-[a-z0-9]+/g, 'svelte-xyz'); } function read(file) { diff --git a/test/custom-elements/assert.js b/test/custom-elements/assert.js deleted file mode 100644 index e4d6f02f33..0000000000 --- a/test/custom-elements/assert.js +++ /dev/null @@ -1,50 +0,0 @@ -export function deepEqual(a, b, message) { - if (!is_equal(a, b)) { - throw new Error(message || `Expected ${JSON.stringify(a)} to equal ${JSON.stringify(b)}`); - } -} - -function is_equal(a, b) { - if (a && typeof a === 'object') { - const is_array = Array.isArray(a); - if (Array.isArray(b) !== is_array) return false; - - if (is_array) { - if (a.length !== b.length) return false; - return a.every((value, i) => is_equal(value, b[i])); - } - - const a_keys = Object.keys(a).sort(); - const b_keys = Object.keys(b).sort(); - if (a_keys.join(',') !== b_keys.join(',')) return false; - - return a_keys.every((key) => is_equal(a[key], b[key])); - } - - return a === b; -} - -export function equal(a, b, message) { - if (a != b) throw new Error(message || `Expected ${a} to equal ${b}`); -} - -export function ok(condition, message) { - if (!condition) throw new Error(message || `Expected ${condition} to be truthy`); -} - -export function htmlEqual(actual, expected, message) { - return deepEqual(normalizeHtml(window, actual), normalizeHtml(window, expected), message); -} - -function normalizeHtml(window, html) { - try { - const node = window.document.createElement('div'); - node.innerHTML = html - .replace(//g, '') - .replace(/>[\s\r\n]+<') - .trim(); - return node.innerHTML.replace(/<\/?noscript\/?>/g, ''); - } catch (err) { - throw new Error(`Failed to normalize HTML:\n${html}`); - } -} diff --git a/test/custom-elements/custom-elements.test.js b/test/custom-elements/custom-elements.test.js deleted file mode 100644 index 2083a39951..0000000000 --- a/test/custom-elements/custom-elements.test.js +++ /dev/null @@ -1,118 +0,0 @@ -import { chromium } from '@playwright/test'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { rollup } from 'rollup'; -import { try_load_config } from '../helpers.js'; -import * as svelte from '../../src/compiler/index.js'; -import { beforeAll, describe, afterAll, assert, it } from 'vitest'; - -const internal = path.resolve('src/runtime/internal/index.js'); -const index = path.resolve('src/runtime/index.js'); - -const browser_assert = fs.readFileSync(`${__dirname}/assert.js`, 'utf-8'); - -describe( - 'custom-elements', - () => { - /** @type {import('@playwright/test').Browser} */ - let browser; - - beforeAll(async () => { - browser = await chromium.launch(); - console.log('[custom-elements] Launched browser'); - }, 20000); - - afterAll(async () => { - if (browser) await browser.close(); - }); - - fs.readdirSync(`${__dirname}/samples`).forEach((dir) => { - if (dir[0] === '.') return; - - const solo = /\.solo$/.test(dir); - const skip = /\.skip$/.test(dir); - - const warnings = []; - const it_fn = solo ? it.only : skip ? it.skip : it; - - it_fn(dir, async () => { - // TODO: Vitest currently doesn't register a watcher because the import is hidden - const config = await try_load_config(`${__dirname}/samples/${dir}/_config.js`); - - const expected_warnings = config.warnings || []; - - const bundle = await rollup({ - input: `${__dirname}/samples/${dir}/test.js`, - plugins: [ - { - name: 'plugin-resolve-svelte', - resolveId(importee) { - if (importee === 'svelte/internal' || importee === './internal') { - return internal; - } - - if (importee === 'svelte') { - return index; - } - - if (importee === 'assert.js') { - return '\0virtual:assert'; - } - }, - - load(id) { - if (id === '\0virtual:assert') return browser_assert; - }, - - transform(code, id) { - if (id.endsWith('.svelte')) { - const compiled = svelte.compile(code.replace(/\r/g, ''), { - customElement: true, - dev: config.dev - }); - - compiled.warnings.forEach((w) => warnings.push(w)); - - return compiled.js; - } - } - } - ] - }); - - const generated_bundle = await bundle.generate({ format: 'iife', name: 'test' }); - - function assertWarnings() { - if (expected_warnings) { - assert.deepStrictEqual( - warnings.map((w) => ({ - code: w.code, - message: w.message, - pos: w.pos, - start: w.start, - end: w.end - })), - expected_warnings - ); - } - } - - const page = await browser.newPage(); - page.on('console', (type) => { - console[type.type()](type.text()); - }); - await page.setContent('
'); - await page.evaluate(generated_bundle.output[0].code); - const test_result = await page.evaluate("test(document.querySelector('main'))"); - - if (test_result) console.log(test_result); - - assertWarnings(); - - await page.close(); - }); - }); - }, - // Browser tests are brittle and slow on CI - { timeout: 20000, retry: process.env.CI ? 1 : 0 } -); diff --git a/test/helpers.js b/test/helpers.js index 112c5c28bd..5dfa80e542 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -3,7 +3,7 @@ import * as path from 'node:path'; import glob from 'tiny-glob/sync'; import colors from 'kleur'; import { assert } from 'vitest'; -import { compile } from '../src/compiler/index.js'; +import { compile } from 'svelte/compiler'; import { fileURLToPath } from 'node:url'; export function try_load_json(file) { diff --git a/test/js/js-output.test.js b/test/js/js-output.test.js index 3fd2b106cb..e608d5cbdf 100644 --- a/test/js/js-output.test.js +++ b/test/js/js-output.test.js @@ -2,7 +2,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { describe, it, assert } from 'vitest'; import { try_load_config, should_update_expected } from '../helpers.js'; -import * as svelte from '../../src/compiler/index.js'; +import * as svelte from 'svelte/compiler'; describe('js-output', () => { fs.readdirSync(`${__dirname}/samples`).forEach((dir) => { diff --git a/test/parser/parser.test.js b/test/parser/parser.test.js index 0ac62ab070..67a621582a 100644 --- a/test/parser/parser.test.js +++ b/test/parser/parser.test.js @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import { assert, describe, it } from 'vitest'; -import * as svelte from '../../src/compiler/index.js'; +import * as svelte from 'svelte/compiler'; import { try_load_json } from '../helpers.js'; describe('parse', () => { diff --git a/test/preprocess/preprocess.test.js b/test/preprocess/preprocess.test.js index 8f7e368bb7..1615468ee0 100644 --- a/test/preprocess/preprocess.test.js +++ b/test/preprocess/preprocess.test.js @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; -import * as svelte from '../../src/compiler/index.js'; -import { try_load_config } from '../helpers.js'; +import * as svelte from 'svelte/compiler'; import { describe, it } from 'vitest'; +import { try_load_config } from '../helpers.js'; const samples = fs.readdirSync(`${__dirname}/samples`); diff --git a/test/runtime-browser/browser.test.js b/test/runtime-browser/browser.test.js index a84ec67244..09eb43cf0b 100644 --- a/test/runtime-browser/browser.test.js +++ b/test/runtime-browser/browser.test.js @@ -1,173 +1,227 @@ import { chromium } from '@playwright/test'; +import { build } from 'esbuild'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { rollup } from 'rollup'; +import * as svelte from 'svelte/compiler'; +import { afterAll, assert, beforeAll, describe, it } from 'vitest'; import { pretty_print_browser_assertion, try_load_config } from '../helpers.js'; -import * as svelte from '../../src/compiler/index.js'; -import { beforeAll, describe, afterAll, assert } from 'vitest'; const internal = path.resolve('src/runtime/internal/index.js'); const index = path.resolve('src/runtime/index.js'); -const main = fs.readFileSync(`${__dirname}/driver.js`, 'utf-8'); -const browser_assert = fs.readFileSync(`${__dirname}/assert.js`, 'utf-8'); +/** @type {import('@playwright/test').Browser} */ +let browser; -describe( +beforeAll(async () => { + browser = await chromium.launch(); + console.log('[runtime-browser] Launched browser'); +}, 20000); + +afterAll(async () => { + if (browser) await browser.close(); +}); + +describe.concurrent( 'runtime (browser)', - async (it) => { - /** @type {import('@playwright/test').Browser} */ - let browser; - - beforeAll(async () => { - browser = await chromium.launch(); - console.log('[runtime-browser] Launched browser'); - }); - - afterAll(async () => { - if (browser) await browser.close(); - }); - - const failed = new Set(); - - async function runTest(dir, hydrate) { - if (dir[0] === '.') return; - - // TODO: Vitest currently doesn't register a watcher because the import is hidden - const config = await try_load_config(`${__dirname}/samples/${dir}/_config.js`); - const solo = config.solo || /\.solo/.test(dir); - const skip = config.skip || /\.skip/.test(dir); - - if (hydrate && config.skip_if_hydrate) return; - - const it_fn = skip ? it.skip : solo ? it.only : it; - - it_fn(`${dir} ${hydrate ? '(with hydration)' : ''}`, async () => { - if (failed.has(dir)) { - // this makes debugging easier, by only printing compiled output once - throw new Error('skipping test, already failed'); - } - - const warnings = []; - - const bundle = await rollup({ - input: 'main', - plugins: [ - { - name: 'testing-runtime-browser', - resolveId(importee) { - if (importee === 'svelte/internal' || importee === './internal') { - return internal; - } - - if (importee === 'svelte') { - return index; - } - - if (importee === 'main') { - return '\0virtual:main'; - } - - if (importee === 'assert.js') { - return '\0virtual:assert'; - } - - if (importee === '__MAIN_DOT_SVELTE__') { - return path.resolve(__dirname, 'samples', dir, 'main.svelte'); - } - - if (importee === '__CONFIG__') { - return path.resolve(__dirname, 'samples', dir, '_config.js'); - } - }, - load(id) { - if (id === '\0virtual:assert') return browser_assert; - - if (id === '\0virtual:main') { - return main.replace('__HYDRATE__', hydrate ? 'true' : 'false'); - } - return null; - }, - transform(code, id) { - if (id.endsWith('.svelte')) { - const compiled = svelte.compile(code.replace(/\r/g, ''), { - ...config.compileOptions, - hydratable: hydrate, - immutable: config.immutable, - accessors: 'accessors' in config ? config.accessors : true - }); - - const out_dir = `${__dirname}/samples/${dir}/_output/${ - hydrate ? 'hydratable' : 'normal' - }`; - const out = `${out_dir}/${path.basename(id).replace(/\.svelte$/, '.js')}`; - - if (fs.existsSync(out)) { - fs.unlinkSync(out); - } - if (!fs.existsSync(out_dir)) { - fs.mkdirSync(out_dir, { recursive: true }); - } - - fs.writeFileSync(out, compiled.js.code, 'utf8'); - - compiled.warnings.forEach((w) => warnings.push(w)); - - return compiled.js; - } - } - } - ] - }); - - const generated_bundle = await bundle.generate({ format: 'iife', name: 'test' }); - - function assertWarnings() { - if (config.warnings) { - assert.deepStrictEqual( - warnings.map((w) => ({ - code: w.code, - message: w.message, - pos: w.pos, - start: w.start, - end: w.end - })), - config.warnings - ); - } else if (warnings.length) { - failed.add(dir); - /* eslint-disable no-unsafe-finally */ - throw new Error('Received unexpected warnings'); - } - } - - try { - const page = await browser.newPage(); - page.on('console', (type) => { - console[type.type()](type.text()); - }); - await page.setContent('
'); - await page.evaluate(generated_bundle.output[0].code); - const test_result = await page.evaluate("test(document.querySelector('main'))"); - - if (test_result) console.log(test_result); - assertWarnings(); - await page.close(); - } catch (err) { - failed.add(dir); - pretty_print_browser_assertion(err.message); - assertWarnings(); - throw err; - } - }); - } - + async () => { await Promise.all( fs.readdirSync(`${__dirname}/samples`).map(async (dir) => { - await runTest(dir, false); - await runTest(dir, true); + await run_browser_test(dir); }) ); }, // Browser tests are brittle and slow on CI { timeout: 20000, retry: process.env.CI ? 1 : 0 } ); + +async function run_browser_test(dir) { + if (dir[0] === '.') return; + + const cwd = `${__dirname}/samples/${dir}`; + + // TODO: Vitest currently doesn't register a watcher because the import is hidden + const config = await try_load_config(`${cwd}/_config.js`); + const solo = config.solo || /\.solo/.test(dir); + const skip = config.skip || /\.skip/.test(dir); + + const it_fn = skip ? it.skip : solo ? it.only : it; + + let failed = false; + it_fn.each([false, true])(`${dir} hydrate: %s`, async (hydrate) => { + if (hydrate && config.skip_if_hydrate) return; + if (failed) { + // this makes debugging easier, by only printing compiled output once + assert.fail('skipping test, already failed'); + } + + const warnings = []; + + const build_result = await build({ + entryPoints: [`${__dirname}/driver.js`], + write: false, + alias: { + __MAIN_DOT_SVELTE__: path.resolve(__dirname, 'samples', dir, 'main.svelte'), + __CONFIG__: path.resolve(__dirname, 'samples', dir, '_config.js'), + 'assert.js': path.resolve(__dirname, 'assert.js'), + 'svelte/internal': internal, + svelte: index + }, + plugins: [ + { + name: 'testing-runtime-browser', + setup(build) { + build.onLoad({ filter: /\.svelte$/ }, ({ path }) => { + const compiled = svelte.compile(fs.readFileSync(path, 'utf-8').replace(/\r/g, ''), { + ...config.compileOptions, + hydratable: hydrate, + immutable: config.immutable, + accessors: 'accessors' in config ? config.accessors : true + }); + + compiled.warnings.forEach((warning) => warnings.push(warning)); + + return { + contents: compiled.js.code, + loader: 'js' + }; + }); + } + } + ], + define: { + __HYDRATE__: hydrate ? 'true' : 'false' + }, + bundle: true, + format: 'iife', + globalName: 'test' + }); + + function assertWarnings() { + if (config.warnings) { + assert.deepStrictEqual( + warnings.map((w) => ({ + code: w.code, + message: w.message, + pos: w.pos, + start: w.start, + end: w.end + })), + config.warnings + ); + } else if (warnings.length) { + failed = true; + /* eslint-disable no-unsafe-finally */ + throw new Error('Received unexpected warnings'); + } + } + + assertWarnings(); + + try { + const page = await browser.newPage(); + page.on('console', (type) => { + console[type.type()](type.text()); + }); + await page.setContent('
'); + await page.evaluate(build_result.outputFiles[0].text); + const test_result = await page.evaluate("test.default(document.querySelector('main'))"); + + if (test_result) console.log(test_result); + await page.close(); + } catch (err) { + failed = true; + pretty_print_browser_assertion(err.message); + throw err; + } + }); +} + +describe.concurrent( + 'custom-elements', + async () => { + await Promise.all( + fs + .readdirSync(`${__dirname}/custom-elements-samples`) + .map((dir) => run_custom_elements_test(dir)) + ); + }, + // Browser tests are brittle and slow on CI + { timeout: 20000, retry: process.env.CI ? 1 : 0 } +); + +async function run_custom_elements_test(dir) { + if (dir[0] === '.') return; + const cwd = `${__dirname}/custom-elements-samples/${dir}`; + + const solo = /\.solo$/.test(dir); + const skip = /\.skip$/.test(dir); + + const warnings = []; + const it_fn = solo ? it.only : skip ? it.skip : it; + + it_fn(dir, async () => { + // TODO: Vitest currently doesn't register a watcher because the import is hidden + const config = await try_load_config(`${cwd}/_config.js`); + + const expected_warnings = config.warnings || []; + + const build_result = await build({ + entryPoints: [`${cwd}/test.js`], + write: false, + alias: { + 'assert.js': path.resolve(__dirname, 'assert.js'), + 'svelte/internal': internal, + svelte: index + }, + plugins: [ + { + name: 'testing-runtime-browser', + setup(build) { + build.onLoad({ filter: /\.svelte$/ }, ({ path }) => { + const compiled = svelte.compile(fs.readFileSync(path, 'utf-8').replace(/\r/g, ''), { + customElement: true, + dev: config.dev + }); + compiled.warnings.forEach((w) => warnings.push(w)); + return { + contents: compiled.js.code, + loader: 'js' + }; + }); + } + } + ], + bundle: true, + format: 'iife', + globalName: 'test' + }); + + function assertWarnings() { + if (expected_warnings) { + assert.deepStrictEqual( + warnings.map((w) => ({ + code: w.code, + message: w.message, + pos: w.pos, + start: w.start, + end: w.end + })), + expected_warnings + ); + } + } + assertWarnings(); + + const page = await browser.newPage(); + page.on('console', (type) => { + console[type.type()](type.text()); + }); + await page.setContent('
'); + await page.evaluate(build_result.outputFiles[0].text); + const test_result = await page.evaluate("test.default(document.querySelector('main'))"); + + if (test_result) console.log(test_result); + + await page.close(); + }); +} diff --git a/test/custom-elements/samples/$$props/main.svelte b/test/runtime-browser/custom-elements-samples/$$props/main.svelte similarity index 100% rename from test/custom-elements/samples/$$props/main.svelte rename to test/runtime-browser/custom-elements-samples/$$props/main.svelte diff --git a/test/custom-elements/samples/$$props/test.js b/test/runtime-browser/custom-elements-samples/$$props/test.js similarity index 100% rename from test/custom-elements/samples/$$props/test.js rename to test/runtime-browser/custom-elements-samples/$$props/test.js diff --git a/test/custom-elements/samples/$$slot-dynamic-content/main.svelte b/test/runtime-browser/custom-elements-samples/$$slot-dynamic-content/main.svelte similarity index 100% rename from test/custom-elements/samples/$$slot-dynamic-content/main.svelte rename to test/runtime-browser/custom-elements-samples/$$slot-dynamic-content/main.svelte diff --git a/test/custom-elements/samples/$$slot-dynamic-content/my-widget.svelte b/test/runtime-browser/custom-elements-samples/$$slot-dynamic-content/my-widget.svelte similarity index 100% rename from test/custom-elements/samples/$$slot-dynamic-content/my-widget.svelte rename to test/runtime-browser/custom-elements-samples/$$slot-dynamic-content/my-widget.svelte diff --git a/test/custom-elements/samples/$$slot-dynamic-content/test.js b/test/runtime-browser/custom-elements-samples/$$slot-dynamic-content/test.js similarity index 100% rename from test/custom-elements/samples/$$slot-dynamic-content/test.js rename to test/runtime-browser/custom-elements-samples/$$slot-dynamic-content/test.js diff --git a/test/custom-elements/samples/$$slot/main.svelte b/test/runtime-browser/custom-elements-samples/$$slot/main.svelte similarity index 100% rename from test/custom-elements/samples/$$slot/main.svelte rename to test/runtime-browser/custom-elements-samples/$$slot/main.svelte diff --git a/test/custom-elements/samples/$$slot/test.js b/test/runtime-browser/custom-elements-samples/$$slot/test.js similarity index 100% rename from test/custom-elements/samples/$$slot/test.js rename to test/runtime-browser/custom-elements-samples/$$slot/test.js diff --git a/test/custom-elements/samples/action/main.svelte b/test/runtime-browser/custom-elements-samples/action/main.svelte similarity index 100% rename from test/custom-elements/samples/action/main.svelte rename to test/runtime-browser/custom-elements-samples/action/main.svelte diff --git a/test/custom-elements/samples/action/test.js b/test/runtime-browser/custom-elements-samples/action/test.js similarity index 100% rename from test/custom-elements/samples/action/test.js rename to test/runtime-browser/custom-elements-samples/action/test.js diff --git a/test/custom-elements/samples/camel-case-attribute/main.svelte b/test/runtime-browser/custom-elements-samples/camel-case-attribute/main.svelte similarity index 100% rename from test/custom-elements/samples/camel-case-attribute/main.svelte rename to test/runtime-browser/custom-elements-samples/camel-case-attribute/main.svelte diff --git a/test/custom-elements/samples/camel-case-attribute/test.js b/test/runtime-browser/custom-elements-samples/camel-case-attribute/test.js similarity index 100% rename from test/custom-elements/samples/camel-case-attribute/test.js rename to test/runtime-browser/custom-elements-samples/camel-case-attribute/test.js diff --git a/test/custom-elements/samples/ce-options-valid/main.svelte b/test/runtime-browser/custom-elements-samples/ce-options-valid/main.svelte similarity index 100% rename from test/custom-elements/samples/ce-options-valid/main.svelte rename to test/runtime-browser/custom-elements-samples/ce-options-valid/main.svelte diff --git a/test/custom-elements/samples/ce-options-valid/test.js b/test/runtime-browser/custom-elements-samples/ce-options-valid/test.js similarity index 100% rename from test/custom-elements/samples/ce-options-valid/test.js rename to test/runtime-browser/custom-elements-samples/ce-options-valid/test.js diff --git a/test/custom-elements/samples/custom-method/main.svelte b/test/runtime-browser/custom-elements-samples/custom-method/main.svelte similarity index 100% rename from test/custom-elements/samples/custom-method/main.svelte rename to test/runtime-browser/custom-elements-samples/custom-method/main.svelte diff --git a/test/custom-elements/samples/custom-method/test.js b/test/runtime-browser/custom-elements-samples/custom-method/test.js similarity index 100% rename from test/custom-elements/samples/custom-method/test.js rename to test/runtime-browser/custom-elements-samples/custom-method/test.js diff --git a/test/custom-elements/samples/escaped-css/main.svelte b/test/runtime-browser/custom-elements-samples/escaped-css/main.svelte similarity index 100% rename from test/custom-elements/samples/escaped-css/main.svelte rename to test/runtime-browser/custom-elements-samples/escaped-css/main.svelte diff --git a/test/custom-elements/samples/escaped-css/test.js b/test/runtime-browser/custom-elements-samples/escaped-css/test.js similarity index 100% rename from test/custom-elements/samples/escaped-css/test.js rename to test/runtime-browser/custom-elements-samples/escaped-css/test.js diff --git a/test/custom-elements/samples/events/main.svelte b/test/runtime-browser/custom-elements-samples/events/main.svelte similarity index 100% rename from test/custom-elements/samples/events/main.svelte rename to test/runtime-browser/custom-elements-samples/events/main.svelte diff --git a/test/custom-elements/samples/events/test.js b/test/runtime-browser/custom-elements-samples/events/test.js similarity index 100% rename from test/custom-elements/samples/events/test.js rename to test/runtime-browser/custom-elements-samples/events/test.js diff --git a/test/custom-elements/samples/extended-builtin/_config.js b/test/runtime-browser/custom-elements-samples/extended-builtin/_config.js similarity index 100% rename from test/custom-elements/samples/extended-builtin/_config.js rename to test/runtime-browser/custom-elements-samples/extended-builtin/_config.js diff --git a/test/custom-elements/samples/extended-builtin/custom-button.js b/test/runtime-browser/custom-elements-samples/extended-builtin/custom-button.js similarity index 100% rename from test/custom-elements/samples/extended-builtin/custom-button.js rename to test/runtime-browser/custom-elements-samples/extended-builtin/custom-button.js diff --git a/test/custom-elements/samples/extended-builtin/main.svelte b/test/runtime-browser/custom-elements-samples/extended-builtin/main.svelte similarity index 100% rename from test/custom-elements/samples/extended-builtin/main.svelte rename to test/runtime-browser/custom-elements-samples/extended-builtin/main.svelte diff --git a/test/custom-elements/samples/extended-builtin/test.js b/test/runtime-browser/custom-elements-samples/extended-builtin/test.js similarity index 100% rename from test/custom-elements/samples/extended-builtin/test.js rename to test/runtime-browser/custom-elements-samples/extended-builtin/test.js diff --git a/test/custom-elements/samples/html-slots/main.svelte b/test/runtime-browser/custom-elements-samples/html-slots/main.svelte similarity index 100% rename from test/custom-elements/samples/html-slots/main.svelte rename to test/runtime-browser/custom-elements-samples/html-slots/main.svelte diff --git a/test/custom-elements/samples/html-slots/test.js b/test/runtime-browser/custom-elements-samples/html-slots/test.js similarity index 100% rename from test/custom-elements/samples/html-slots/test.js rename to test/runtime-browser/custom-elements-samples/html-slots/test.js diff --git a/test/custom-elements/samples/html/main.svelte b/test/runtime-browser/custom-elements-samples/html/main.svelte similarity index 100% rename from test/custom-elements/samples/html/main.svelte rename to test/runtime-browser/custom-elements-samples/html/main.svelte diff --git a/test/custom-elements/samples/html/test.js b/test/runtime-browser/custom-elements-samples/html/test.js similarity index 100% rename from test/custom-elements/samples/html/test.js rename to test/runtime-browser/custom-elements-samples/html/test.js diff --git a/test/custom-elements/samples/nested/Counter.svelte b/test/runtime-browser/custom-elements-samples/nested/Counter.svelte similarity index 100% rename from test/custom-elements/samples/nested/Counter.svelte rename to test/runtime-browser/custom-elements-samples/nested/Counter.svelte diff --git a/test/custom-elements/samples/nested/main.svelte b/test/runtime-browser/custom-elements-samples/nested/main.svelte similarity index 100% rename from test/custom-elements/samples/nested/main.svelte rename to test/runtime-browser/custom-elements-samples/nested/main.svelte diff --git a/test/custom-elements/samples/nested/test.js b/test/runtime-browser/custom-elements-samples/nested/test.js similarity index 100% rename from test/custom-elements/samples/nested/test.js rename to test/runtime-browser/custom-elements-samples/nested/test.js diff --git a/test/custom-elements/samples/new-styled/main.svelte b/test/runtime-browser/custom-elements-samples/new-styled/main.svelte similarity index 100% rename from test/custom-elements/samples/new-styled/main.svelte rename to test/runtime-browser/custom-elements-samples/new-styled/main.svelte diff --git a/test/custom-elements/samples/new-styled/test.js b/test/runtime-browser/custom-elements-samples/new-styled/test.js similarity index 100% rename from test/custom-elements/samples/new-styled/test.js rename to test/runtime-browser/custom-elements-samples/new-styled/test.js diff --git a/test/custom-elements/samples/no-missing-prop-warnings/_config.js b/test/runtime-browser/custom-elements-samples/no-missing-prop-warnings/_config.js similarity index 100% rename from test/custom-elements/samples/no-missing-prop-warnings/_config.js rename to test/runtime-browser/custom-elements-samples/no-missing-prop-warnings/_config.js diff --git a/test/custom-elements/samples/no-missing-prop-warnings/main.svelte b/test/runtime-browser/custom-elements-samples/no-missing-prop-warnings/main.svelte similarity index 100% rename from test/custom-elements/samples/no-missing-prop-warnings/main.svelte rename to test/runtime-browser/custom-elements-samples/no-missing-prop-warnings/main.svelte diff --git a/test/custom-elements/samples/no-missing-prop-warnings/test.js b/test/runtime-browser/custom-elements-samples/no-missing-prop-warnings/test.js similarity index 100% rename from test/custom-elements/samples/no-missing-prop-warnings/test.js rename to test/runtime-browser/custom-elements-samples/no-missing-prop-warnings/test.js diff --git a/test/custom-elements/samples/no-shadow-dom/main.svelte b/test/runtime-browser/custom-elements-samples/no-shadow-dom/main.svelte similarity index 100% rename from test/custom-elements/samples/no-shadow-dom/main.svelte rename to test/runtime-browser/custom-elements-samples/no-shadow-dom/main.svelte diff --git a/test/custom-elements/samples/no-shadow-dom/test.js b/test/runtime-browser/custom-elements-samples/no-shadow-dom/test.js similarity index 100% rename from test/custom-elements/samples/no-shadow-dom/test.js rename to test/runtime-browser/custom-elements-samples/no-shadow-dom/test.js diff --git a/test/custom-elements/samples/no-tag/_config.js b/test/runtime-browser/custom-elements-samples/no-tag/_config.js similarity index 100% rename from test/custom-elements/samples/no-tag/_config.js rename to test/runtime-browser/custom-elements-samples/no-tag/_config.js diff --git a/test/custom-elements/samples/no-tag/main.svelte b/test/runtime-browser/custom-elements-samples/no-tag/main.svelte similarity index 100% rename from test/custom-elements/samples/no-tag/main.svelte rename to test/runtime-browser/custom-elements-samples/no-tag/main.svelte diff --git a/test/custom-elements/samples/no-tag/test.js b/test/runtime-browser/custom-elements-samples/no-tag/test.js similarity index 100% rename from test/custom-elements/samples/no-tag/test.js rename to test/runtime-browser/custom-elements-samples/no-tag/test.js diff --git a/test/custom-elements/samples/oncreate/main.svelte b/test/runtime-browser/custom-elements-samples/oncreate/main.svelte similarity index 100% rename from test/custom-elements/samples/oncreate/main.svelte rename to test/runtime-browser/custom-elements-samples/oncreate/main.svelte diff --git a/test/custom-elements/samples/oncreate/test.js b/test/runtime-browser/custom-elements-samples/oncreate/test.js similarity index 100% rename from test/custom-elements/samples/oncreate/test.js rename to test/runtime-browser/custom-elements-samples/oncreate/test.js diff --git a/test/custom-elements/samples/ondestroy/main.svelte b/test/runtime-browser/custom-elements-samples/ondestroy/main.svelte similarity index 100% rename from test/custom-elements/samples/ondestroy/main.svelte rename to test/runtime-browser/custom-elements-samples/ondestroy/main.svelte diff --git a/test/custom-elements/samples/ondestroy/test.js b/test/runtime-browser/custom-elements-samples/ondestroy/test.js similarity index 100% rename from test/custom-elements/samples/ondestroy/test.js rename to test/runtime-browser/custom-elements-samples/ondestroy/test.js diff --git a/test/custom-elements/samples/props/main.svelte b/test/runtime-browser/custom-elements-samples/props/main.svelte similarity index 100% rename from test/custom-elements/samples/props/main.svelte rename to test/runtime-browser/custom-elements-samples/props/main.svelte diff --git a/test/custom-elements/samples/props/my-widget.svelte b/test/runtime-browser/custom-elements-samples/props/my-widget.svelte similarity index 100% rename from test/custom-elements/samples/props/my-widget.svelte rename to test/runtime-browser/custom-elements-samples/props/my-widget.svelte diff --git a/test/custom-elements/samples/props/test.js b/test/runtime-browser/custom-elements-samples/props/test.js similarity index 100% rename from test/custom-elements/samples/props/test.js rename to test/runtime-browser/custom-elements-samples/props/test.js diff --git a/test/custom-elements/samples/reflect-attributes/main.svelte b/test/runtime-browser/custom-elements-samples/reflect-attributes/main.svelte similarity index 100% rename from test/custom-elements/samples/reflect-attributes/main.svelte rename to test/runtime-browser/custom-elements-samples/reflect-attributes/main.svelte diff --git a/test/custom-elements/samples/reflect-attributes/my-widget.svelte b/test/runtime-browser/custom-elements-samples/reflect-attributes/my-widget.svelte similarity index 100% rename from test/custom-elements/samples/reflect-attributes/my-widget.svelte rename to test/runtime-browser/custom-elements-samples/reflect-attributes/my-widget.svelte diff --git a/test/custom-elements/samples/reflect-attributes/test.js b/test/runtime-browser/custom-elements-samples/reflect-attributes/test.js similarity index 100% rename from test/custom-elements/samples/reflect-attributes/test.js rename to test/runtime-browser/custom-elements-samples/reflect-attributes/test.js diff --git a/test/runtime/runtime.shared.js b/test/runtime/runtime.shared.js new file mode 100644 index 0000000000..95a648771b --- /dev/null +++ b/test/runtime/runtime.shared.js @@ -0,0 +1,272 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { setImmediate } from 'node:timers/promises'; +import { compile } from 'svelte/compiler'; +import { clear_loops, flush, set_now, set_raf } from 'svelte/internal'; +import glob from 'tiny-glob/sync.js'; +import { afterAll, assert, beforeAll, describe, it } from 'vitest'; +import { create_loader, mkdirp, try_load_config } from '../helpers.js'; +import { setup_html_equal } from '../html_equal.js'; + +let unhandled_rejection = false; +function unhandled_rejection_handler(err) { + unhandled_rejection = err; +} + +const listeners = process.rawListeners('unhandledRejection'); + +const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal({ + removeDataSvelte: true +}); + +beforeAll(() => { + process.prependListener('unhandledRejection', unhandled_rejection_handler); +}); + +afterAll(() => { + process.removeListener('unhandledRejection', unhandled_rejection_handler); +}); + +const failed = new Set(); + +async function run_test(dir) { + if (dir[0] === '.') return; + + const config = await try_load_config(`${__dirname}/samples/${dir}/_config.js`); + const solo = config.solo || /\.solo/.test(dir); + + const it_fn = config.skip ? it.skip : solo ? it.only : it; + + it_fn.each` + hydrate | from_ssr_html + ${false} | ${false} + ${true} | ${false} + ${true} | ${true} + `(`${dir} hydrate: $hydrate, from_ssr: $from_ssr_html`, async ({ hydrate, from_ssr_html }) => { + if (hydrate && config.skip_if_hydrate) return; + if (hydrate && from_ssr_html && config.skip_if_hydrate_from_ssr) return; + + if (failed.has(dir)) { + // this makes debugging easier, by only printing compiled output once + assert.fail(`skipping ${dir}, already failed`); + } + + unhandled_rejection = null; + + const cwd = path.resolve(`${__dirname}/samples/${dir}`); + + const compileOptions = Object.assign({}, config.compileOptions || {}, { + format: 'cjs', + hydratable: hydrate, + immutable: config.immutable, + accessors: 'accessors' in config ? config.accessors : true + }); + + const load = create_loader(compileOptions, cwd); + + let mod; + let SvelteComponent; + + let unintendedError = null; + + if (config.expect_unhandled_rejections) { + listeners.forEach((listener) => { + // @ts-expect-error + process.removeListener('unhandledRejection', listener); + }); + } + + async function test() { + // hack to support transition tests + clear_loops(); + + const raf = { + time: 0, + callback: null, + tick: (now) => { + raf.time = now; + if (raf.callback) raf.callback(); + } + }; + set_now(() => raf.time); + set_raf((cb) => { + raf.callback = () => { + raf.callback = null; + cb(raf.time); + flush(); + }; + }); + + mod = await load('./main.svelte'); + SvelteComponent = mod.default; + + // Put things we need on window for testing + // @ts-expect-error + window.SvelteComponent = SvelteComponent; + window.location.href = ''; + window.document.title = ''; + window.document.head.innerHTML = ''; + window.document.body.innerHTML = '
'; + + const target = window.document.querySelector('main'); + let snapshot = undefined; + + if (hydrate && from_ssr_html) { + const load_ssr = create_loader({ ...compileOptions, generate: 'ssr' }, cwd); + + // ssr into target + if (config.before_test) config.before_test(); + const SsrSvelteComponent = (await load_ssr('./main.svelte')).default; + const { html } = SsrSvelteComponent.render(config.props); + target.innerHTML = html; + + if (config.snapshot) { + snapshot = config.snapshot(target); + } + + if (config.after_test) config.after_test(); + } else { + target.innerHTML = ''; + } + + if (config.before_test) config.before_test(); + + const warnings = []; + const warn = console.warn; + console.warn = (warning) => { + warnings.push(warning); + }; + + const options = Object.assign( + {}, + { + target, + hydrate, + props: config.props, + intro: config.intro + }, + config.options || {} + ); + + const component = new SvelteComponent(options); + + console.warn = warn; + + if (config.error) { + unintendedError = true; + assert.fail('Expected a runtime error'); + } + + if (config.warnings) { + assert.deepEqual(warnings, config.warnings); + } else if (warnings.length) { + unintendedError = true; + assert.fail('Received unexpected warnings'); + } + + if (config.html) { + assert_html_equal_with_options(target.innerHTML, config.html, { + withoutNormalizeHtml: config.withoutNormalizeHtml + }); + } + + try { + if (config.test) { + await config.test({ + assert: { + ...assert, + htmlEqual: assert_html_equal, + htmlEqualWithOptions: assert_html_equal_with_options + }, + component, + mod, + target, + snapshot, + window, + raf, + compileOptions, + load + }); + } + } finally { + component.$destroy(); + assert_html_equal(target.innerHTML, ''); + + // TODO: This seems useless, unhandledRejection is only triggered on the next task + // by which time the test has already finished and the next test resets it to null above + if (unhandled_rejection) { + throw unhandled_rejection; // eslint-disable-line no-unsafe-finally + } + } + } + + await test() + .catch((err) => { + if (config.error && !unintendedError) { + if (typeof config.error === 'function') { + config.error(assert, err); + } else { + assert.equal(err.message, config.error); + } + } else { + for (const file of glob('**/*.svelte', { cwd })) { + if (file[0] === '_') continue; + + const dir = `${cwd}/_output/${hydrate ? 'hydratable' : 'normal'}`; + const out = `${dir}/${file.replace(/\.svelte$/, '.js')}`; + + mkdirp(dir); + + const { js } = compile(fs.readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r/g, ''), { + ...compileOptions, + filename: file + }); + fs.writeFileSync(out, js.code); + } + + throw err; + } + }) + .catch((err) => { + failed.add(dir); + // print a clickable link to open the directory + err.stack += `\n\ncmd-click: ${path.relative(process.cwd(), cwd)}/main.svelte`; + + throw err; + }) + .finally(async () => { + flush(); + + if (config.after_test) config.after_test(); + + // Free up the microtask queue + // 1. Vitest's test runner which uses setInterval can log progress + // 2. Any expected unhandled rejections are ran before we reattach the listeners + await setImmediate(); + + if (config.expect_unhandled_rejections) { + listeners.forEach((listener) => { + // @ts-expect-error + process.on('unhandledRejection', listener); + }); + } + }); + }); +} + +// There are a lot of tests in this suite, which take up a lot of time. +// Split them into groups so that they can run in parallel and finish faster. +export function run_shard(id, total_shards) { + assert.isAtMost(id, total_shards); + + const samples = fs.readdirSync(`${__dirname}/samples`); + const shard_size = Math.ceil(samples.length / total_shards); + + const start = (id - 1) * shard_size; + const end = id * shard_size; + const to_run = samples.slice(start, end); + + describe(`runtime_${id}`, async () => { + await Promise.all(to_run.map((dir) => run_test(dir))); + }); +} diff --git a/test/runtime/runtime.test.js b/test/runtime/runtime.test.js deleted file mode 100644 index 004045db3f..0000000000 --- a/test/runtime/runtime.test.js +++ /dev/null @@ -1,292 +0,0 @@ -// @vitest-environment jsdom - -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import glob from 'tiny-glob/sync.js'; -import { beforeAll, afterAll, describe, it, assert } from 'vitest'; -import { compile } from '../../src/compiler/index.js'; -import { clear_loops, flush, set_now, set_raf } from 'svelte/internal'; -import { show_output, try_load_config, mkdirp, create_loader } from '../helpers.js'; -import { setTimeout } from 'node:timers/promises'; -import { setup_html_equal } from '../html_equal.js'; - -let unhandled_rejection = false; -function unhandledRejection_handler(err) { - unhandled_rejection = err; -} - -const listeners = process.rawListeners('unhandledRejection'); - -const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal({ - removeDataSvelte: true -}); - -describe('runtime', async () => { - beforeAll(() => { - process.prependListener('unhandledRejection', unhandledRejection_handler); - }); - - afterAll(() => { - process.removeListener('unhandledRejection', unhandledRejection_handler); - }); - - const failed = new Set(); - - async function run_test(dir) { - if (dir[0] === '.') return; - - const config = await try_load_config(`${__dirname}/samples/${dir}/_config.js`); - const solo = config.solo || /\.solo/.test(dir); - - const it_fn = config.skip ? it.skip : solo ? it.only : it; - - it_fn.each` - hydrate | from_ssr_html - ${false} | ${false} - ${true} | ${false} - ${true} | ${true} - `(`${dir} hydrate: $hydrate, from_ssr: $from_ssr_html`, async ({ hydrate, from_ssr_html }) => { - if (hydrate && config.skip_if_hydrate) return; - if (hydrate && from_ssr_html && config.skip_if_hydrate_from_ssr) return; - - if (failed.has(dir)) { - // this makes debugging easier, by only printing compiled output once - assert.fail(`skipping ${dir}, already failed`); - } - - unhandled_rejection = null; - - const cwd = path.resolve(`${__dirname}/samples/${dir}`); - - const compileOptions = Object.assign({}, config.compileOptions || {}, { - format: 'cjs', - hydratable: hydrate, - immutable: config.immutable, - accessors: 'accessors' in config ? config.accessors : true - }); - - const load = create_loader(compileOptions, cwd); - - let mod; - let SvelteComponent; - - let unintendedError = null; - - glob('**/*.svelte', { cwd }).forEach((file) => { - if (file[0] === '_') return; - - const dir = `${cwd}/_output/${hydrate ? 'hydratable' : 'normal'}`; - const out = `${dir}/${file.replace(/\.svelte$/, '.js')}`; - - if (fs.existsSync(out)) { - fs.unlinkSync(out); - } - - mkdirp(dir); - - try { - const { js } = compile(fs.readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r/g, ''), { - ...compileOptions, - filename: file - }); - - fs.writeFileSync(out, js.code); - } catch (err) { - // do nothing - } - }); - - if (config.expect_unhandled_rejections) { - listeners.forEach((listener) => { - process.removeListener('unhandledRejection', listener); - }); - } - - await Promise.resolve() - .then(async () => { - // hack to support transition tests - clear_loops(); - - const raf = { - time: 0, - callback: null, - tick: (now) => { - raf.time = now; - if (raf.callback) raf.callback(); - } - }; - set_now(() => raf.time); - set_raf((cb) => { - raf.callback = () => { - raf.callback = null; - cb(raf.time); - flush(); - }; - }); - - try { - mod = await load('./main.svelte'); - SvelteComponent = mod.default; - } catch (err) { - show_output(cwd, compileOptions); // eslint-disable-line no-console - throw err; - } - - // Put things we need on window for testing - // @ts-ignore - window.SvelteComponent = SvelteComponent; - window.location.href = ''; - window.document.title = ''; - window.document.head.innerHTML = ''; - window.document.body.innerHTML = '
'; - - const target = window.document.querySelector('main'); - let snapshot = undefined; - - if (hydrate && from_ssr_html) { - const load_ssr = create_loader({ ...compileOptions, generate: 'ssr' }, cwd); - - // ssr into target - if (config.before_test) config.before_test(); - const SsrSvelteComponent = (await load_ssr('./main.svelte')).default; - const { html } = SsrSvelteComponent.render(config.props); - target.innerHTML = html; - - if (config.snapshot) { - snapshot = config.snapshot(target); - } - - if (config.after_test) config.after_test(); - } else { - target.innerHTML = ''; - } - - if (config.before_test) config.before_test(); - - const warnings = []; - const warn = console.warn; - console.warn = (warning) => { - warnings.push(warning); - }; - - const options = Object.assign( - {}, - { - target, - hydrate, - props: config.props, - intro: config.intro - }, - config.options || {} - ); - - const component = new SvelteComponent(options); - - console.warn = warn; - - if (config.error) { - unintendedError = true; - assert.fail('Expected a runtime error'); - } - - if (config.warnings) { - assert.deepEqual(warnings, config.warnings); - } else if (warnings.length) { - unintendedError = true; - assert.fail('Received unexpected warnings'); - } - - if (config.html) { - assert_html_equal_with_options(target.innerHTML, config.html, { - withoutNormalizeHtml: config.withoutNormalizeHtml - }); - } - - try { - if (config.test) { - await config.test({ - assert: { - ...assert, - htmlEqual: assert_html_equal, - htmlEqualWithOptions: assert_html_equal_with_options - }, - component, - mod, - target, - snapshot, - window, - raf, - compileOptions, - load - }); - } - } finally { - component.$destroy(); - assert_html_equal(target.innerHTML, ''); - - // TODO: This seems useless, unhandledRejection is only triggered on the next task - // by which time the test has already finished and the next test resets it to null above - if (unhandled_rejection) { - // eslint-disable-next-line no-unsafe-finally - throw unhandled_rejection; - } - } - }) - .catch((err) => { - if (config.error && !unintendedError) { - if (typeof config.error === 'function') { - config.error(assert, err); - } else { - assert.equal(err.message, config.error); - } - } else { - throw err; - } - }) - .catch((err) => { - failed.add(dir); - // print a clickable link to open the directory - err.stack += `\n\ncmd-click: ${path.relative(process.cwd(), cwd)}/main.svelte`; - - throw err; - }) - .finally(async () => { - flush(); - - if (config.after_test) config.after_test(); - - // Free up the microtask queue, so that - // 1. Vitest's test runner which uses setInterval can log progress - // 2. Any expected unhandled rejections are ran before we reattach the listeners - await setTimeout(0); - - if (config.expect_unhandled_rejections) { - listeners.forEach((listener) => { - process.on('unhandledRejection', listener); - }); - } - }); - }); - } - - const samples = fs.readdirSync(`${__dirname}/samples`); - await Promise.all(samples.map((sample) => run_test(sample))); - - const load = create_loader({ generate: 'dom', dev: true, format: 'cjs' }, __dirname); - const { default: App } = await load('App.svelte'); - - it('fails if options.target is missing in dev mode', async () => { - assert.throws(() => { - new App(); - }, /'target' is a required option/); - }); - - it('fails if options.hydrate is true but the component is non-hydratable', async () => { - assert.throws(() => { - new App({ - target: { childNodes: [] }, - hydrate: true - }); - }, /options\.hydrate only works if the component was compiled with the `hydratable: true` option/); - }); -}); diff --git a/test/runtime/runtime_base.test.js b/test/runtime/runtime_base.test.js new file mode 100644 index 0000000000..756c63c187 --- /dev/null +++ b/test/runtime/runtime_base.test.js @@ -0,0 +1,22 @@ +// @vitest-environment jsdom + +import { create_loader } from '../helpers'; +import { assert, it } from 'vitest'; + +const load = create_loader({ generate: 'dom', dev: true, format: 'cjs' }, __dirname); +const { default: App } = await load('App.svelte'); + +it('fails if options.target is missing in dev mode', async () => { + assert.throws(() => { + new App(); + }, /'target' is a required option/); +}); + +it('fails if options.hydrate is true but the component is non-hydratable', async () => { + assert.throws(() => { + new App({ + target: { childNodes: [] }, + hydrate: true + }); + }, /options\.hydrate only works if the component was compiled with the `hydratable: true` option/); +}); diff --git a/test/server-side-rendering/ssr-2.test.js b/test/server-side-rendering/ssr-2.test.js index a0780f04f6..acf823d0b0 100644 --- a/test/server-side-rendering/ssr-2.test.js +++ b/test/server-side-rendering/ssr-2.test.js @@ -2,10 +2,10 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { setTimeout } from 'node:timers/promises'; +import { setImmediate } from 'node:timers/promises'; import glob from 'tiny-glob/sync'; import { assert, describe, it } from 'vitest'; -import { compile } from '../../src/compiler/index.js'; +import { compile } from 'svelte/compiler'; import { create_loader, mkdirp, try_load_config } from '../helpers.js'; import { assert_html_equal, assert_html_equal_with_options } from '../html_equal.js'; @@ -39,30 +39,6 @@ function run_runtime_samples(suite) { const load = create_loader(compileOptions, cwd); - glob('**/*.svelte', { cwd }).forEach((file) => { - if (file[0] === '_') return; - - const dir = `${cwd}/_output/ssr`; - const out = `${dir}/${file.replace(/\.svelte$/, '.js')}`; - - if (fs.existsSync(out)) { - fs.unlinkSync(out); - } - - mkdirp(dir); - - try { - const { js } = compile(fs.readFileSync(`${cwd}/${file}`, 'utf-8'), { - ...compileOptions, - filename: file - }); - - fs.writeFileSync(out, js.code); - } catch (err) { - // do nothing - } - }); - try { if (config.before_test) config.before_test(); @@ -105,12 +81,32 @@ function run_runtime_samples(suite) { assert.equal(err.message, config.error); } } else { + glob('**/*.svelte', { cwd }).forEach((file) => { + if (file[0] === '_') return; + + const dir = `${cwd}/_output/ssr`; + const out = `${dir}/${file.replace(/\.svelte$/, '.js')}`; + + if (fs.existsSync(out)) { + fs.unlinkSync(out); + } + + mkdirp(dir); + + const { js } = compile(fs.readFileSync(`${cwd}/${file}`, 'utf-8'), { + ...compileOptions, + filename: file + }); + + fs.writeFileSync(out, js.code); + }); + throw err; } } // wait for vitest to report progress - await setTimeout(0); + await setImmediate(); }); } } diff --git a/test/sourcemaps/sourcemaps.test.js b/test/sourcemaps/sourcemaps.test.js index 7ae7c1336e..d00ba0fc2d 100644 --- a/test/sourcemaps/sourcemaps.test.js +++ b/test/sourcemaps/sourcemaps.test.js @@ -1,8 +1,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import * as svelte from '../../src/compiler/index.js'; +import * as svelte from 'svelte/compiler'; +import { assert, describe, it } from 'vitest'; import { try_load_config } from '../helpers.js'; -import { describe, assert, it } from 'vitest'; // keep source-map at version 0.7.x // https://github.com/mozilla/source-map/issues/400 import { getLocator } from 'locate-character'; diff --git a/test/stats/stats.test.js b/test/stats/stats.test.js index b5875a89ff..4abb771c4c 100644 --- a/test/stats/stats.test.js +++ b/test/stats/stats.test.js @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import { describe, it, assert } from 'vitest'; -import * as svelte from '../../src/compiler/index.js'; +import * as svelte from 'svelte/compiler'; import { try_load_config, try_load_json } from '../helpers.js'; describe('stats', () => { diff --git a/test/validator/validator.test.js b/test/validator/validator.test.js index ca15ca23ce..e23cf4d45a 100644 --- a/test/validator/validator.test.js +++ b/test/validator/validator.test.js @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import { describe, it, assert } from 'vitest'; -import * as svelte from '../../src/compiler/index.js'; +import * as svelte from 'svelte/compiler'; import { try_load_json, try_load_config } from '../helpers.js'; describe('validate', () => { diff --git a/test/vars/vars.test.js b/test/vars/vars.test.js index eaee475b03..9e786377ac 100644 --- a/test/vars/vars.test.js +++ b/test/vars/vars.test.js @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import { assert, describe, it } from 'vitest'; -import { compile } from '../../src/compiler/index.js'; +import { compile } from 'svelte/compiler'; import { try_load_json } from '../helpers.js'; describe('vars', () => { diff --git a/test/vitest-global-setup.js b/test/vitest-global-setup.js new file mode 100644 index 0000000000..f18cebb6af --- /dev/null +++ b/test/vitest-global-setup.js @@ -0,0 +1,27 @@ +import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; + +// There are a lot of tests in the runtime suite, which take up a lot of time. +// Split them into groups so that they can run in parallel and finish faster. +// Vitest parallelizes tests on a per-file basis, so this hack creates a bunch +// of files that run a subset of the tests. +function create_shard_files() { + let num_shards = +process.env.SVELTE_TEST_SUITE_SHARDS || 1; + num_shards = Math.max(1, num_shards); + + const runtime_shards_dir = `${__dirname}/runtime/shards`; + rmSync(runtime_shards_dir, { recursive: true, force: true }); + mkdirSync(runtime_shards_dir); + + for (let i = 1; i <= num_shards; i += 1) { + writeFileSync( + `${runtime_shards_dir}/runtime_${i}.test.js`, + `// @vitest-environment jsdom + import { run_shard } from '../runtime.shared.js'; + run_shard(${i}, ${num_shards});`.replaceAll('\t', '') + ); + } +} + +export default function () { + create_shard_files(); +} diff --git a/vitest.config.js b/vitest.config.js index eda8875f3d..4392caa0e1 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,13 +1,18 @@ -import { defineConfig, configDefaults } from 'vitest/config'; +import { configDefaults, defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [ { name: 'resolve-svelte', resolveId(id) { + if (id === 'svelte/compiler') { + return `${__dirname}/src/compiler/index.js`; + } + if (id === 'svelte') { return `${__dirname}/src/runtime/index.js`; } + if (id.startsWith('svelte/')) { return id.replace(/^svelte(.*)\/?$/, `${__dirname}/src/runtime/$1/index.js`); } @@ -17,6 +22,7 @@ export default defineConfig({ test: { dir: 'test', reporters: ['dot'], - exclude: [...configDefaults.exclude, '**/samples/**'] + exclude: [...configDefaults.exclude, '**/samples/**'], + globalSetup: './test/vitest-global-setup.js' } });