From e7c6b1c2a6abdc3d1224467b22f19dec92d36e13 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Thu, 11 Sep 2025 17:38:31 -0600 Subject: [PATCH] test improvements --- .github/workflows/ci.yml | 1 - .../src/internal/client/dom/operations.js | 2 +- .../src/internal/client/dom/template.js | 3 + packages/svelte/src/internal/server/index.js | 8 ++- .../svelte/tests/runtime-legacy/shared.ts | 60 +++++++++++++------ .../async-no-pending-throws-sync/_config.js | 7 +++ .../async-no-pending-throws-sync/main.svelte | 1 + .../samples/async-no-pending/_config.js | 2 + .../tests/server-side-rendering/test.ts | 14 +++++ 9 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/main.svelte diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94fb391258..51408fc8cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,6 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm playwright install chromium - run: pnpm test runtime-runes - - run: pnpm test server-side-rendering env: CI: true SVELTE_NO_ASYNC: true diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index f79d2e66cd..c527ca23e3 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -130,7 +130,7 @@ export function child(node, is_text) { /** * Don't mark this as side-effect-free, hydration needs to walk all nodes - * @param {DocumentFragment | TemplateNode[]} fragment + * @param {DocumentFragment | TemplateNode | TemplateNode[]} fragment * @param {boolean} [is_text] * @returns {Node | null} */ diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 265a52262f..135ca86610 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -316,6 +316,9 @@ export function text(value = '') { return node; } +/** + * @returns {TemplateNode | DocumentFragment} + */ export function comment() { // we're not delegating to `template` here for performance reasons if (hydrating) { diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 6bd4383710..c13e1d3f64 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -68,7 +68,9 @@ export let on_destroy = []; */ export function render(component, options = {}) { try { - const payload = new Payload(new TreeState(options.idPrefix ? options.idPrefix + '-' : '')); + const payload = new Payload( + new TreeState('sync', options.idPrefix ? options.idPrefix + '-' : '') + ); const prev_on_destroy = on_destroy; on_destroy = []; @@ -137,7 +139,9 @@ export let async_on_destroy = []; */ export async function render_async(component, options = {}) { try { - const payload = new Payload(new TreeState(options.idPrefix ? options.idPrefix + '-' : '')); + const payload = new Payload( + new TreeState('async', options.idPrefix ? options.idPrefix + '-' : '') + ); const prev_on_destroy = async_on_destroy; async_on_destroy = []; diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 630117abd3..efd581f6ee 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -43,9 +43,9 @@ Promise.withResolvers = () => { export interface RuntimeTest = Record> extends BaseTest { /** Use e.g. `mode: ['client']` to indicate that this test should never run in server/hydrate modes */ - mode?: Array<'server' | 'client' | 'hydrate'>; + mode?: Array<'server' | 'async-server' | 'client' | 'hydrate'>; /** Temporarily skip specific modes, without skipping the entire test */ - skip_mode?: Array<'server' | 'client' | 'hydrate'>; + skip_mode?: Array<'server' | 'async-server' | 'client' | 'hydrate'>; /** Skip if running with process.env.NO_ASYNC */ skip_no_async?: boolean; /** Skip if running without process.env.NO_ASYNC */ @@ -83,7 +83,11 @@ export interface RuntimeTest = Record void | Promise; - test_ssr?: (args: { logs: any[]; assert: Assert }) => void | Promise; + test_ssr?: (args: { + logs: any[]; + assert: Assert; + variant: 'ssr' | 'async-ssr'; + }) => void | Promise; accessors?: boolean; immutable?: boolean; intro?: boolean; @@ -124,7 +128,7 @@ let console_warn = console.warn; let console_error = console.error; export function runtime_suite(runes: boolean) { - return suite_with_variants( + return suite_with_variants( ['dom', 'hydrate', 'ssr'], (variant, config, test_name) => { if (!async_mode && (config.skip_no_async || test_name.startsWith('async-'))) { @@ -162,6 +166,21 @@ export function runtime_suite(runes: boolean) { if (config.skip_mode?.includes('server')) return true; } + if (variant === 'async-ssr') { + if ( + (config.mode && !config.mode.includes('async-server')) || + (!config.test_ssr && + config.html === undefined && + config.ssrHtml === undefined && + config.error === undefined && + config.runtime_error === undefined && + !config.mode?.includes('async-server')) + ) { + return 'no-test'; + } + if (config.skip_mode?.includes('async-server')) return true; + } + return false; }, (config, cwd) => { @@ -207,7 +226,7 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run async function run_test_variant( cwd: string, config: RuntimeTest, - variant: 'dom' | 'hydrate' | 'ssr', + variant: 'dom' | 'hydrate' | 'ssr' | 'async-ssr', compileOptions: CompileOptions, runes: boolean ) { @@ -310,26 +329,28 @@ async function run_test_variant( let snapshot = undefined; - if (variant === 'hydrate' || variant === 'ssr') { + if (variant === 'hydrate' || variant === 'ssr' || variant === 'async-ssr') { config.before_test?.(); // ssr into target const SsrSvelteComponent = (await import(`${cwd}/_output/server/main.svelte.js`)).default; - const rendered = async_mode - ? await renderAsync(SsrSvelteComponent, { - props: config.server_props ?? config.props ?? {}, - idPrefix: config.id_prefix - }) - : render(SsrSvelteComponent, { - props: config.server_props ?? config.props ?? {}, - idPrefix: config.id_prefix - }); + const rendered = + variant === 'async-ssr' || variant === 'hydrate' + ? await renderAsync(SsrSvelteComponent, { + props: config.server_props ?? config.props ?? {}, + idPrefix: config.id_prefix + }) + : render(SsrSvelteComponent, { + props: config.server_props ?? config.props ?? {}, + idPrefix: config.id_prefix + }); const { html, head } = rendered; - fs.writeFileSync(`${cwd}/_output/rendered.html`, html); + const prefix = variant === 'async-ssr' ? 'async_' : ''; + fs.writeFileSync(`${cwd}/_output/${prefix}rendered.html`, html); target.innerHTML = html; if (head) { - fs.writeFileSync(`${cwd}/_output/rendered_head.html`, head); + fs.writeFileSync(`${cwd}/_output/${prefix}rendered_head.html`, head); window.document.head.innerHTML = window.document.head.innerHTML + head; } @@ -344,7 +365,7 @@ async function run_test_variant( target.innerHTML = ''; } - if (variant === 'ssr') { + if (variant === 'ssr' || variant === 'async-ssr') { if (config.ssrHtml) { assert_html_equal_with_options(target.innerHTML, config.ssrHtml, { preserveComments: @@ -367,7 +388,8 @@ async function run_test_variant( ...assert, htmlEqual: assert_html_equal, htmlEqualWithOptions: assert_html_equal_with_options - } + }, + variant }); } } else { diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/_config.js b/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/_config.js new file mode 100644 index 0000000000..6211eec854 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + mode: ['server'], + + error: 'Encountered an asynchronous component while rendering synchronously' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/main.svelte new file mode 100644 index 0000000000..4b8cd7203b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/main.svelte @@ -0,0 +1 @@ +

{await Promise.resolve('hello')}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending/_config.js b/packages/svelte/tests/runtime-runes/samples/async-no-pending/_config.js index 98ab1b9772..91b516e7c9 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-no-pending/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending/_config.js @@ -2,6 +2,8 @@ import { tick } from 'svelte'; import { ok, test } from '../../test'; export default test({ + skip_mode: ['server'], + ssrHtml: '

hello

', html: '', diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts index 676f51b0c9..f1326ae6b4 100644 --- a/packages/svelte/tests/server-side-rendering/test.ts +++ b/packages/svelte/tests/server-side-rendering/test.ts @@ -26,6 +26,20 @@ interface SSRTest extends BaseTest { errors?: string[]; } +// TODO remove this shim when we can +// @ts-expect-error +Promise.withResolvers = () => { + let resolve; + let reject; + + const promise = new Promise((f, r) => { + resolve = f; + reject = r; + }); + + return { promise, resolve, reject }; +}; + // eslint-disable-next-line no-console let console_error = console.error;