From bce1d76aad9533c1fd6db99c05526c3566fecaee Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Thu, 22 Jul 2021 15:49:36 -0700 Subject: [PATCH] [chore] improved typing for runtime and tests (#1995) --- .changeset/itchy-days-wonder.md | 5 + packages/kit/src/core/adapter-utils.js | 2 +- packages/kit/src/core/dev/index.js | 8 +- packages/kit/src/core/start/index.js | 2 +- packages/kit/src/runtime/app/navigation.js | 16 +++- packages/kit/src/runtime/client/renderer.js | 21 +++-- packages/kit/src/runtime/client/router.js | 15 +-- packages/kit/src/runtime/client/singletons.js | 4 +- packages/kit/src/runtime/client/start.js | 21 +++-- packages/kit/src/runtime/server/endpoint.js | 91 ++++++++++--------- packages/kit/src/runtime/server/index.js | 5 +- packages/kit/src/runtime/server/page/index.js | 22 ++--- .../kit/src/runtime/server/page/load_node.js | 10 +- .../kit/src/runtime/server/page/render.js | 2 +- .../kit/src/runtime/server/page/resolve.js | 4 + .../runtime/server/page/respond_with_error.js | 2 +- .../src/runtime/server/parse_body/index.js | 13 ++- .../server/parse_body/read_only_form_data.js | 2 +- .../basics/src/routes/accessibility/_tests.js | 12 +-- .../test/apps/basics/src/routes/css/_tests.js | 22 +++-- .../apps/basics/src/routes/errors/_tests.js | 23 +++-- .../apps/basics/src/routes/etag/_tests.js | 16 ++-- .../apps/basics/src/routes/load/_tests.js | 7 +- .../apps/basics/src/routes/no-ssr/_tests.js | 7 +- .../apps/basics/src/routes/routing/_tests.js | 2 +- .../src/routes/unsafe-replacement/_tests.js | 4 +- packages/kit/test/types.d.ts | 2 +- packages/kit/types/endpoint.d.ts | 2 +- packages/kit/types/hooks.d.ts | 2 +- packages/kit/types/internal.d.ts | 8 +- packages/kit/types/page.d.ts | 4 +- 31 files changed, 203 insertions(+), 153 deletions(-) create mode 100644 .changeset/itchy-days-wonder.md diff --git a/.changeset/itchy-days-wonder.md b/.changeset/itchy-days-wonder.md new file mode 100644 index 000000000000..16883ed7d2a8 --- /dev/null +++ b/.changeset/itchy-days-wonder.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[chore] improved typing for runtime and tests diff --git a/packages/kit/src/core/adapter-utils.js b/packages/kit/src/core/adapter-utils.js index fadbda59204b..ddd911eb4253 100644 --- a/packages/kit/src/core/adapter-utils.js +++ b/packages/kit/src/core/adapter-utils.js @@ -3,7 +3,7 @@ * * This is intended to be used with both requests and responses, to have a consistent body parsing across adapters. * - * @param {string?} content_type The `content-type` header of a request/response. + * @param {string|undefined|null} content_type The `content-type` header of a request/response. * @returns {boolean} */ export function isContentTypeTextual(content_type) { diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index d4688ae8d84f..039c040e134b 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -18,7 +18,7 @@ import { get_server } from '../server/index.js'; import { __fetch_polyfill } from '../../install-fetch.js'; import { SVELTE_KIT } from '../constants.js'; -/** @typedef {{ cwd?: string, port: number, host: string, https: boolean, config: import('types/config').ValidatedConfig }} Options */ +/** @typedef {{ cwd?: string, port: number, host?: string, https: boolean, config: import('types/config').ValidatedConfig }} Options */ /** @typedef {import('types/internal').SSRComponent} SSRComponent */ /** @param {Options} opts */ @@ -47,9 +47,8 @@ class Watcher extends EventEmitter { this.config = config; /** - * @type {vite.ViteDevServer} + * @type {vite.ViteDevServer | undefined} */ - // @ts-ignore this.vite; process.on('exit', () => { @@ -198,6 +197,7 @@ class Watcher extends EventEmitter { pattern: route.pattern, params: get_params(route.params), load: async () => { + if (!this.vite) throw new Error('Vite server has not been initialized'); const url = path.resolve(this.cwd, route.file); return await this.vite.ssrLoadModule(url); } @@ -281,7 +281,7 @@ async function create_handler(vite, config, dir, cwd, manifest) { if (req.url === '/favicon.ico') return; - /** @type {import('types/internal').Hooks} */ + /** @type {Partial} */ const hooks = resolve_entry(config.kit.files.hooks) ? await vite.ssrLoadModule(`/${config.kit.files.hooks}`) : {}; diff --git a/packages/kit/src/core/start/index.js b/packages/kit/src/core/start/index.js index fe798c22e187..8ae6808b5487 100644 --- a/packages/kit/src/core/start/index.js +++ b/packages/kit/src/core/start/index.js @@ -17,7 +17,7 @@ const mutable = (dir) => /** * @param {{ * port: number; - * host: string; + * host?: string; * config: import('types/config').ValidatedConfig; * https?: boolean; * cwd?: string; diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index b8f2266bdac5..e3e4ba049bd0 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -19,6 +19,7 @@ export const prefetchRoutes = import.meta.env.SSR ? guard('prefetchRoutes') : pr * @type {import('$app/navigation').goto} */ async function goto_(href, opts) { + // @ts-ignore return router.goto(href, opts, []); } @@ -27,6 +28,7 @@ async function goto_(href, opts) { */ async function invalidate_(resource) { const { href } = new URL(resource, location.href); + // @ts-ignore return router.renderer.invalidate(href); } @@ -34,6 +36,7 @@ async function invalidate_(resource) { * @type {import('$app/navigation').prefetch} */ function prefetch_(href) { + // @ts-ignore return router.prefetch(new URL(href, get_base_uri(document))); } @@ -42,10 +45,15 @@ function prefetch_(href) { */ async function prefetchRoutes_(pathnames) { const matching = pathnames - ? router.routes.filter((route) => pathnames.some((pathname) => route[0].test(pathname))) - : router.routes; - - const promises = matching.map((r) => r.length !== 1 && Promise.all(r[1].map((load) => load()))); + ? // @ts-ignore + router.routes.filter((route) => pathnames.some((pathname) => route[0].test(pathname))) + : // @ts-ignore + router.routes; + + const promises = matching + .filter((r) => r && r.length > 1) + // @ts-ignore + .map((r) => Promise.all(r[1].map((load) => load()))); await Promise.all(promises); } diff --git a/packages/kit/src/runtime/client/renderer.js b/packages/kit/src/runtime/client/renderer.js index 572432dbc688..74aab7c5c8f7 100644 --- a/packages/kit/src/runtime/client/renderer.js +++ b/packages/kit/src/runtime/client/renderer.js @@ -46,7 +46,7 @@ function initial_fetch(resource, opts) { } const script = document.querySelector(selector); - if (script) { + if (script && script.textContent) { const { body, ...init } = JSON.parse(script.textContent); return Promise.resolve(new Response(body, init)); } @@ -69,8 +69,8 @@ export class Renderer { this.fallback = fallback; this.host = host; - /** @type {import('./router').Router} */ - this.router = null; + /** @type {import('./router').Router | undefined} */ + this.router; this.target = target; @@ -133,13 +133,13 @@ export class Renderer { /** @type {Record} */ let context = {}; - /** @type {import('./types').NavigationResult} */ + /** @type {import('./types').NavigationResult | undefined} */ let result; - /** @type {number} */ + /** @type {number | undefined} */ let new_status; - /** @type {Error} new_error */ + /** @type {Error | undefined} new_error */ let new_error; try { @@ -150,8 +150,8 @@ export class Renderer { module: await nodes[i], page, context, - status: is_leaf && status, - error: is_leaf && error + status: is_leaf ? status : undefined, + error: is_leaf ? error : undefined }); branch.push(node); @@ -535,8 +535,9 @@ export class Renderer { async _load({ route, path, query }, no_cache) { const key = `${path}?${query}`; - if (!no_cache && this.cache.has(key)) { - return this.cache.get(key); + if (!no_cache) { + const cached = this.cache.get(key); + if (cached) return cached; } const [pattern, a, b, get_params] = route; diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index d9f71096abbe..d72d367ba939 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -8,8 +8,8 @@ function scroll_state() { } /** - * @param {Node} node - * @returns {HTMLAnchorElement | SVGAElement} + * @param {Node | null} node + * @returns {HTMLAnchorElement | SVGAElement | null} */ function find_anchor(node) { while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG elements have a lowercase name @@ -121,7 +121,7 @@ export class Router { // Ignore if tag has // 1. 'download' attribute // 2. 'rel' attribute includes external - const rel = a.getAttribute('rel') && a.getAttribute('rel').split(/\s+/); + const rel = (a.getAttribute('rel') || '').split(/\s+/); if (a.hasAttribute('download') || (rel && rel.includes('external'))) { return; @@ -162,7 +162,7 @@ export class Router { /** * @param {URL} url - * @returns {import('./types').NavigationInfo} + * @returns {import('./types').NavigationInfo | undefined} */ parse(url) { if (this.owns(url)) { @@ -221,12 +221,13 @@ export class Router { throw new Error('Attempted to prefetch a URL that does not belong to this app'); } + // @ts-ignore return this.renderer.load(info); } /** * @param {URL} url - * @param {{ x: number, y: number }} scroll + * @param {{ x: number, y: number }?} scroll * @param {boolean} keepfocus * @param {string[]} chain * @param {string} [hash] @@ -246,7 +247,7 @@ export class Router { (has_trailing_slash && this.trailing_slash === 'never') || (!has_trailing_slash && this.trailing_slash === 'always' && - !info.path.split('/').pop().includes('.')); + !(info.path.split('/').pop() || '').includes('.')); if (incorrect) { info.path = has_trailing_slash ? info.path.slice(0, -1) : info.path + '/'; @@ -254,11 +255,13 @@ export class Router { } } + // @ts-ignore6 this.renderer.notify({ path: info.path, query: info.query }); + // @ts-ignore await this.renderer.update(info, chain, false); if (!keepfocus) { diff --git a/packages/kit/src/runtime/client/singletons.js b/packages/kit/src/runtime/client/singletons.js index 5a12299409ae..e77fa01b72b6 100644 --- a/packages/kit/src/runtime/client/singletons.js +++ b/packages/kit/src/runtime/client/singletons.js @@ -1,4 +1,4 @@ -/** @type {import('./router').Router} */ +/** @type {import('./router').Router?} */ export let router; /** @type {string} */ @@ -7,7 +7,7 @@ export let base = ''; /** @type {string} */ export let assets = '/.'; -/** @param {import('./router').Router} _ */ +/** @param {import('./router').Router?} _ */ export function init(_) { router = _; } diff --git a/packages/kit/src/runtime/client/start.js b/packages/kit/src/runtime/client/start.js index f53fba4e013b..e88bfbf6fb47 100644 --- a/packages/kit/src/runtime/client/start.js +++ b/packages/kit/src/runtime/client/start.js @@ -30,13 +30,13 @@ export async function start({ paths, target, session, host, route, spa, trailing throw new Error('Missing target element. See https://kit.svelte.dev/docs#configuration-target'); } - const router = - route && - new Router({ - base: paths.base, - routes, - trailing_slash - }); + const router = route + ? new Router({ + base: paths.base, + routes, + trailing_slash + }) + : null; const renderer = new Renderer({ Root, @@ -50,9 +50,10 @@ export async function start({ paths, target, session, host, route, spa, trailing set_paths(paths); if (hydrate) await renderer.start(hydrate); - if (route) router.init(renderer); - - if (spa) router.goto(location.href, { replaceState: true }, []); + if (router) { + router.init(renderer); + if (spa) router.goto(location.href, { replaceState: true }, []); + } dispatchEvent(new CustomEvent('sveltekit:start')); } diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index ebeb89a05512..c532e2c10b41 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -26,47 +26,54 @@ export default async function render_route(request, route) { /** @type {import('types/endpoint').RequestHandler} */ const handler = mod[request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word - if (handler) { - const match = route.pattern.exec(request.path); - const params = route.params(match); - - const response = await handler({ ...request, params }); - const preface = `Invalid response from route ${request.path}`; - - if (response) { - if (typeof response !== 'object') { - return error(`${preface}: expected an object, got ${typeof response}`); - } - - let { status = 200, body, headers = {} } = response; - - headers = lowercase_keys(headers); - const type = headers['content-type']; - - const is_type_textual = isContentTypeTextual(type); - - if (!is_type_textual && !(body instanceof Uint8Array || is_string(body))) { - return error( - `${preface}: body must be an instance of string or Uint8Array if content-type is not a supported textual content-type` - ); - } - - /** @type {import('types/hooks').StrictBody} */ - let normalized_body; - - // ensure the body is an object - if ( - (typeof body === 'object' || typeof body === 'undefined') && - !(body instanceof Uint8Array) && - (!type || type.startsWith('application/json')) - ) { - headers = { ...headers, 'content-type': 'application/json; charset=utf-8' }; - normalized_body = JSON.stringify(typeof body === 'undefined' ? {} : body); - } else { - normalized_body = /** @type {import('types/hooks').StrictBody} */ (body); - } - - return { status, body: normalized_body, headers }; - } + if (!handler) { + return error('no handler'); } + + const match = route.pattern.exec(request.path); + if (!match) { + return error('could not parse parameters from request path'); + } + + const params = route.params(match); + + const response = await handler({ ...request, params }); + const preface = `Invalid response from route ${request.path}`; + + if (!response) { + return error('no response'); + } + if (typeof response !== 'object') { + return error(`${preface}: expected an object, got ${typeof response}`); + } + + let { status = 200, body, headers = {} } = response; + + headers = lowercase_keys(headers); + const type = headers['content-type']; + + const is_type_textual = isContentTypeTextual(type); + + if (!is_type_textual && !(body instanceof Uint8Array || is_string(body))) { + return error( + `${preface}: body must be an instance of string or Uint8Array if content-type is not a supported textual content-type` + ); + } + + /** @type {import('types/hooks').StrictBody} */ + let normalized_body; + + // ensure the body is an object + if ( + (typeof body === 'object' || typeof body === 'undefined') && + !(body instanceof Uint8Array) && + (!type || type.startsWith('application/json')) + ) { + headers = { ...headers, 'content-type': 'application/json; charset=utf-8' }; + normalized_body = JSON.stringify(typeof body === 'undefined' ? {} : body); + } else { + normalized_body = /** @type {import('types/hooks').StrictBody} */ (body); + } + + return { status, body: normalized_body, headers }; } diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index c9e9d02632e6..f31955a7045b 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -18,7 +18,7 @@ export async function respond(incoming, options, state = {}) { (has_trailing_slash && options.trailing_slash === 'never') || (!has_trailing_slash && options.trailing_slash === 'always' && - !incoming.path.split('/').pop().includes('.')) + !(incoming.path.split('/').pop() || '').includes('.')) ) { const path = has_trailing_slash ? incoming.path.slice(0, -1) : incoming.path + '/'; const q = incoming.query.toString(); @@ -40,7 +40,7 @@ export async function respond(incoming, options, state = {}) { ...incoming, headers, body: parse_body(incoming.rawBody, headers), - params: null, + params: {}, locals: {} }, resolve: async (request) => { @@ -50,7 +50,6 @@ export async function respond(incoming, options, state = {}) { $session: await options.hooks.getSession(request), page_config: { ssr: false, router: true, hydrate: true }, status: 200, - error: null, branch: [], page: null }); diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index d14dd8b09a90..fb0bf6860eff 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -3,7 +3,7 @@ import { respond_with_error } from './respond_with_error.js'; /** * @param {import('types/hooks').ServerRequest} request - * @param {import('types/internal').SSRPage} route + * @param {import('types/internal').SSRPage | null} route * @param {import('types/internal').SSRRenderOptions} options * @param {import('types/internal').SSRRenderState} state * @returns {Promise} @@ -33,17 +33,15 @@ export default async function render_page(request, route, options, state) { return response; } - if (state.fetched) { - // we came here because of a bad request in a `load` function. - // rather than render the error page — which could lead to an - // infinite loop, if the `load` belonged to the root layout, - // we respond with a bare-bones 500 - return { - status: 500, - headers: {}, - body: `Bad request in load function: failed to fetch ${state.fetched}` - }; - } + // we came here because of a bad request in a `load` function. + // rather than render the error page — which could lead to an + // infinite loop, if the `load` belonged to the root layout, + // we respond with a bare-bones 500 + return { + status: 500, + headers: {}, + body: `Bad request in load function: failed to fetch ${state.fetched}` + }; } else { return await respond_with_error({ request, diff --git a/packages/kit/src/runtime/server/page/load_node.js b/packages/kit/src/runtime/server/page/load_node.js index 008ca5981452..8d024d1b9433 100644 --- a/packages/kit/src/runtime/server/page/load_node.js +++ b/packages/kit/src/runtime/server/page/load_node.js @@ -10,7 +10,7 @@ const s = JSON.stringify; * request: import('types/hooks').ServerRequest; * options: import('types/internal').SSRRenderOptions; * state: import('types/internal').SSRRenderState; - * route: import('types/internal').SSRPage; + * route: import('types/internal').SSRPage | null; * page: import('types/page').Page; * node: import('types/internal').SSRNode; * $session: any; @@ -99,9 +99,11 @@ export async function load_node({ if (asset) { response = options.read ? new Response(options.read(asset.file), { - headers: { - 'content-type': asset.type - } + headers: asset.type + ? { + 'content-type': asset.type + } + : {} }) : await fetch( // TODO we need to know what protocol to use diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 8a03feec29be..9fb285471fc2 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -210,7 +210,7 @@ function try_serialize(data, fail) { // Ensure we return something truthy so the client will not re-render the page over the error -/** @param {Error & {frame?: string} & {loc?: object}} error */ +/** @param {(Error & {frame?: string} & {loc?: object}) | undefined | null} error */ function serialize_error(error) { if (!error) return null; let serialized = try_serialize(error); diff --git a/packages/kit/src/runtime/server/page/resolve.js b/packages/kit/src/runtime/server/page/resolve.js index c67c23010646..0a0b32a93929 100644 --- a/packages/kit/src/runtime/server/page/resolve.js +++ b/packages/kit/src/runtime/server/page/resolve.js @@ -8,6 +8,10 @@ export function resolve(base, path) { const base_match = absolute.exec(base); const path_match = absolute.exec(path); + if (!base_match) { + throw new Error(`bad base path: "${base}"`); + } + const baseparts = path_match ? [] : base.slice(base_match[0].length).split('/'); const pathparts = path_match ? path.slice(path_match[0].length).split('/') : path.split('/'); diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index db0ebee17266..246d24248284 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -46,7 +46,7 @@ export async function respond_with_error({ request, options, state, $session, st page, node: default_error, $session, - context: loaded.context, + context: loaded ? loaded.context : {}, is_leaf: false, is_error: true, status, diff --git a/packages/kit/src/runtime/server/parse_body/index.js b/packages/kit/src/runtime/server/parse_body/index.js index 9f050b24b9cd..2b12d71309c4 100644 --- a/packages/kit/src/runtime/server/parse_body/index.js +++ b/packages/kit/src/runtime/server/parse_body/index.js @@ -55,18 +55,17 @@ function get_urlencoded(text) { function get_multipart(text, boundary) { const parts = text.split(`--${boundary}`); - const nope = () => { - throw new Error('Malformed form data'); - }; - if (parts[0] !== '' || parts[parts.length - 1].trim() !== '--') { - nope(); + throw new Error('Malformed form data'); } const { data, append } = read_only_form_data(); parts.slice(1, -1).forEach((part) => { const match = /\s*([\s\S]+?)\r\n\r\n([\s\S]*)\s*/.exec(part); + if (!match) { + throw new Error('Malformed form data'); + } const raw_headers = match[1]; const body = match[2].trim(); @@ -89,7 +88,7 @@ function get_multipart(text, boundary) { }); if (name === 'content-disposition') { - if (value !== 'form-data') nope(); + if (value !== 'form-data') throw new Error('Malformed form data'); if (directives.filename) { // TODO we probably don't want to do this automatically @@ -102,7 +101,7 @@ function get_multipart(text, boundary) { } }); - if (!key) nope(); + if (!key) throw new Error('Malformed form data'); append(key, body); }); diff --git a/packages/kit/src/runtime/server/parse_body/read_only_form_data.js b/packages/kit/src/runtime/server/parse_body/read_only_form_data.js index 3268c4ffcc5c..9f4aaeb965ac 100644 --- a/packages/kit/src/runtime/server/parse_body/read_only_form_data.js +++ b/packages/kit/src/runtime/server/parse_body/read_only_form_data.js @@ -9,7 +9,7 @@ export function read_only_form_data() { */ append(key, value) { if (map.has(key)) { - map.get(key).push(value); + (map.get(key) || []).push(value); } else { map.set(key, [value]); } diff --git a/packages/kit/test/apps/basics/src/routes/accessibility/_tests.js b/packages/kit/test/apps/basics/src/routes/accessibility/_tests.js index 4cf3f8f86334..3d26de584148 100644 --- a/packages/kit/test/apps/basics/src/routes/accessibility/_tests.js +++ b/packages/kit/test/apps/basics/src/routes/accessibility/_tests.js @@ -7,20 +7,20 @@ export default function (test) { await clicknav('[href="/accessibility/b"]'); assert.equal(await page.innerHTML('h1'), 'b'); await page.waitForTimeout(50); - assert.equal(await page.evaluate(() => document.activeElement.nodeName), 'BODY'); + assert.equal(await page.evaluate(() => (document.activeElement || {}).nodeName), 'BODY'); await page.keyboard.press('Tab'); await page.waitForTimeout(50); - assert.equal(await page.evaluate(() => document.activeElement.nodeName), 'A'); - assert.equal(await page.evaluate(() => document.activeElement.textContent), 'a'); + assert.equal(await page.evaluate(() => (document.activeElement || {}).nodeName), 'A'); + assert.equal(await page.evaluate(() => (document.activeElement || {}).textContent), 'a'); await clicknav('[href="/accessibility/a"]'); assert.equal(await page.innerHTML('h1'), 'a'); await page.waitForTimeout(50); - assert.equal(await page.evaluate(() => document.activeElement.nodeName), 'BODY'); + assert.equal(await page.evaluate(() => (document.activeElement || {}).nodeName), 'BODY'); await page.keyboard.press('Tab'); await page.waitForTimeout(50); - assert.equal(await page.evaluate(() => document.activeElement.nodeName), 'A'); - assert.equal(await page.evaluate(() => document.activeElement.textContent), 'a'); + assert.equal(await page.evaluate(() => (document.activeElement || {}).nodeName), 'A'); + assert.equal(await page.evaluate(() => (document.activeElement || {}).textContent), 'a'); }); test('announces client-side navigation', '/accessibility/a', async ({ page, clicknav, js }) => { diff --git a/packages/kit/test/apps/basics/src/routes/css/_tests.js b/packages/kit/test/apps/basics/src/routes/css/_tests.js index 14148f0279c9..cf7ac8dda297 100644 --- a/packages/kit/test/apps/basics/src/routes/css/_tests.js +++ b/packages/kit/test/apps/basics/src/routes/css/_tests.js @@ -4,21 +4,30 @@ import * as assert from 'uvu/assert'; export default function (test) { test('applies imported styles', '/css', async ({ page }) => { assert.equal( - await page.evaluate(() => getComputedStyle(document.querySelector('.styled')).color), + await page.evaluate(() => { + const el = document.querySelector('.styled'); + return el && getComputedStyle(el).color; + }), 'rgb(255, 0, 0)' ); }); test('applies layout styles', '/css', async ({ page }) => { assert.equal( - await page.evaluate(() => getComputedStyle(document.querySelector('footer')).color), + await page.evaluate(() => { + const el = document.querySelector('footer'); + return el && getComputedStyle(el).color; + }), 'rgb(128, 0, 128)' ); }); test('applies local styles', '/css', async ({ page }) => { assert.equal( - await page.evaluate(() => getComputedStyle(document.querySelector('.also-styled')).color), + await page.evaluate(() => { + const el = document.querySelector('.also-styled'); + return el && getComputedStyle(el).color; + }), 'rgb(0, 0, 255)' ); }); @@ -31,9 +40,10 @@ export default function (test) { await clicknav('[href="/css/other"]'); assert.equal( - await page.evaluate( - () => getComputedStyle(document.querySelector('#svelte-announcer')).position - ), + await page.evaluate(() => { + const el = document.querySelector('#svelte-announcer'); + return el && getComputedStyle(el).position; + }), 'absolute' ); } diff --git a/packages/kit/test/apps/basics/src/routes/errors/_tests.js b/packages/kit/test/apps/basics/src/routes/errors/_tests.js index d35110aac1dd..2407dc2776c8 100644 --- a/packages/kit/test/apps/basics/src/routes/errors/_tests.js +++ b/packages/kit/test/apps/basics/src/routes/errors/_tests.js @@ -59,7 +59,10 @@ export default function (test, is_dev) { ); assert.equal( - await page.evaluate(() => getComputedStyle(document.querySelector('h1')).color), + await page.evaluate(() => { + const el = document.querySelector('h1'); + return el && getComputedStyle(el).color; + }), 'rgb(255, 0, 0)' ); }); @@ -146,9 +149,10 @@ export default function (test, is_dev) { const body = await page.textContent('body'); assert.ok( - body.includes( - 'Error: "error" property returned from load() must be a string or instance of Error, received type "object"' - ) + body && + body.includes( + 'Error: "error" property returned from load() must be a string or instance of Error, received type "object"' + ) ); } ); @@ -161,9 +165,10 @@ export default function (test, is_dev) { const body = await page.textContent('body'); assert.ok( - await body.includes( - 'Error: "error" property returned from load() must be a string or instance of Error, received type "object"' - ), + body && + body.includes( + 'Error: "error" property returned from load() must be a string or instance of Error, received type "object"' + ), 'Should throw error' ); } @@ -198,12 +203,12 @@ export default function (test, is_dev) { assert.ok(lines[2].includes('endpoint.json'), 'Logs error stack in dev'); } - assert.equal(res.status(), 500); + assert.equal(res && res.status(), 500); assert.equal(await page.textContent('#message'), 'This is your custom error page saying: ""'); const contents = await page.textContent('#stack'); const location = 'endpoint.svelte:12:15'; - const has_stack_trace = contents.includes(location); + const has_stack_trace = contents && contents.includes(location); if (is_dev) { assert.ok(has_stack_trace, `Could not find ${location} in ${contents}`); diff --git a/packages/kit/test/apps/basics/src/routes/etag/_tests.js b/packages/kit/test/apps/basics/src/routes/etag/_tests.js index 0aee3ac6d2c7..3946300d7b7c 100644 --- a/packages/kit/test/apps/basics/src/routes/etag/_tests.js +++ b/packages/kit/test/apps/basics/src/routes/etag/_tests.js @@ -11,9 +11,11 @@ export default function (test) { assert.ok(!!etag); const r2 = await fetch('/etag/text', { - headers: { - 'if-none-match': etag - } + headers: etag + ? { + 'if-none-match': etag + } + : {} }); assert.equal(r2.status, 304); @@ -32,9 +34,11 @@ export default function (test) { assert.ok(!!etag); const r2 = await fetch('/etag/binary', { - headers: { - 'if-none-match': etag - } + headers: etag + ? { + 'if-none-match': etag + } + : {} }); assert.equal(r2.status, 304); diff --git a/packages/kit/test/apps/basics/src/routes/load/_tests.js b/packages/kit/test/apps/basics/src/routes/load/_tests.js index aec10e52a456..d97e030d5405 100644 --- a/packages/kit/test/apps/basics/src/routes/load/_tests.js +++ b/packages/kit/test/apps/basics/src/routes/load/_tests.js @@ -159,7 +159,7 @@ export default function (test, is_dev) { if (!res.write(chunk)) { await new Promise((fulfil) => { res.once('drain', () => { - fulfil(); + fulfil(undefined); }); }); } @@ -170,7 +170,7 @@ export default function (test, is_dev) { }); await new Promise((fulfil) => { - server.listen(port, () => fulfil()); + server.listen(port, () => fulfil(undefined)); }); await page.goto(`${base}/load/large-response?port=${port}`); @@ -188,6 +188,7 @@ export default function (test, is_dev) { const requested_urls = []; const server = http.createServer(async (req, res) => { + if (!req.url) throw new Error('Incomplete request'); requested_urls.push(req.url); if (req.url === '/server-fetch-request-modified.json') { @@ -204,7 +205,7 @@ export default function (test, is_dev) { }); await new Promise((fulfil) => { - server.listen(port, () => fulfil()); + server.listen(port, () => fulfil(undefined)); }); await page.goto(`${base}/load/server-fetch-request?port=${port}`); diff --git a/packages/kit/test/apps/basics/src/routes/no-ssr/_tests.js b/packages/kit/test/apps/basics/src/routes/no-ssr/_tests.js index 6c56ad7d2a48..6e8d3cfa1a85 100644 --- a/packages/kit/test/apps/basics/src/routes/no-ssr/_tests.js +++ b/packages/kit/test/apps/basics/src/routes/no-ssr/_tests.js @@ -19,9 +19,10 @@ export default function (test, is_dev) { await clicknav('[href="/no-ssr/other"]'); assert.equal( - await page.evaluate( - () => getComputedStyle(document.querySelector('#svelte-announcer')).position - ), + await page.evaluate(() => { + const el = document.querySelector('#svelte-announcer'); + return el && getComputedStyle(el).position; + }), 'absolute' ); } diff --git a/packages/kit/test/apps/basics/src/routes/routing/_tests.js b/packages/kit/test/apps/basics/src/routes/routing/_tests.js index 5a90f9be13e3..852706add695 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/_tests.js +++ b/packages/kit/test/apps/basics/src/routes/routing/_tests.js @@ -148,7 +148,7 @@ export default function (test) { test('resets the active element after navigation', '/routing', async ({ page, clicknav }) => { await clicknav('[href="/routing/a"]'); - await page.waitForFunction(() => document.activeElement.nodeName == 'BODY'); + await page.waitForFunction(() => (document.activeElement || {}).nodeName == 'BODY'); }); test( diff --git a/packages/kit/test/apps/basics/src/routes/unsafe-replacement/_tests.js b/packages/kit/test/apps/basics/src/routes/unsafe-replacement/_tests.js index 3593b44b7281..53955d1d244f 100644 --- a/packages/kit/test/apps/basics/src/routes/unsafe-replacement/_tests.js +++ b/packages/kit/test/apps/basics/src/routes/unsafe-replacement/_tests.js @@ -3,6 +3,8 @@ import * as assert from 'uvu/assert'; /** @type {import('test').TestMaker} */ export default function (test) { test('replaces %svelte.xxx% tags safely', '/unsafe-replacement', async ({ page }) => { - assert.match(await page.textContent('body'), '$& $&'); + const content = await page.textContent('body'); + if (!content) throw new Error('No body content'); + assert.match(content, '$& $&'); }); } diff --git a/packages/kit/test/types.d.ts b/packages/kit/test/types.d.ts index b4bdba40e239..1bbc171f1a27 100644 --- a/packages/kit/test/types.d.ts +++ b/packages/kit/test/types.d.ts @@ -46,7 +46,7 @@ type TestOptions = { export interface TestFunctionBase { ( name: string, - start: string, + start: string | null, callback: (context: TestContext) => void, options?: TestOptions ): void; diff --git a/packages/kit/types/endpoint.d.ts b/packages/kit/types/endpoint.d.ts index 6bea8b19b1cb..2687562aebf6 100644 --- a/packages/kit/types/endpoint.d.ts +++ b/packages/kit/types/endpoint.d.ts @@ -14,7 +14,7 @@ type DefaultBody = JSONValue | Uint8Array; export type EndpointOutput = { status?: number; - headers?: Partial; + headers?: Headers; body?: Body; }; diff --git a/packages/kit/types/hooks.d.ts b/packages/kit/types/hooks.d.ts index 99ce1aeab54e..d50776a133f7 100644 --- a/packages/kit/types/hooks.d.ts +++ b/packages/kit/types/hooks.d.ts @@ -1,6 +1,6 @@ import { Headers, Location, MaybePromise, ParameterizedBody } from './helper'; -export type StrictBody = string | Uint8Array | null; +export type StrictBody = string | Uint8Array; export type ServerRequest, Body = unknown> = Location & { method: string; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index d59b7c7a7773..a85bb346ebd5 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -117,9 +117,9 @@ export type SSRManifest = { }; export type Hooks = { - getSession?: GetSession; - handle?: Handle; - serverFetch?: ServerFetch; + getSession: GetSession; + handle: Handle; + serverFetch: ServerFetch; }; export type SSRNode = { @@ -161,7 +161,7 @@ export type SSRRenderOptions = { export type SSRRenderState = { fetched?: string; - initiator?: SSRPage; + initiator?: SSRPage | null; prerender?: { fallback: string; all: boolean; diff --git a/packages/kit/types/page.d.ts b/packages/kit/types/page.d.ts index cf2058d47048..8bdc9888feee 100644 --- a/packages/kit/types/page.d.ts +++ b/packages/kit/types/page.d.ts @@ -16,8 +16,8 @@ export type ErrorLoadInput< Context extends Record = Record, Session = any > = LoadInput & { - status: number; - error: Error; + status?: number; + error?: Error; }; export type LoadOutput<