From 39a91cfabfcb8ecd0202f2b446858d7293f8167d Mon Sep 17 00:00:00 2001 From: ematipico Date: Tue, 31 Mar 2026 11:49:02 +0100 Subject: [PATCH 1/3] chore: move unit tests to ts --- packages/astro/src/core/cookies/cookies.ts | 2 +- .../test/units/_temp-fixtures/package.json | 8 - ...ion-error.test.js => action-error.test.ts} | 12 +- ...ction-path.test.js => action-path.test.ts} | 1 - ...ns-proxy.test.js => actions-proxy.test.ts} | 37 ++-- ...ct.test.js => form-data-to-object.test.ts} | 6 +- .../{serialize.test.js => serialize.test.ts} | 11 +- ...ase-path.test.js => css-base-path.test.ts} | 55 +++-- ...nvalid-css.test.js => invalid-css.test.ts} | 6 +- ...compiler.test.js => rust-compiler.test.ts} | 16 +- ...fig-merge.test.js => config-merge.test.ts} | 8 +- ...resolve.test.js => config-resolve.test.ts} | 0 ...g-server.test.js => config-server.test.ts} | 18 +- ...config.test.js => config-tsconfig.test.ts} | 29 ++- ...lidate.test.js => config-validate.test.ts} | 11 +- ...ry-info.test.js => get-entry-info.test.ts} | 0 ...ry-type.test.js => get-entry-type.test.ts} | 0 ...ences.test.js => image-references.test.ts} | 13 +- ...ore.test.js => mutable-data-store.test.ts} | 2 +- .../{locals.test.js => locals.test.ts} | 6 +- ...redirect.test.js => open-redirect.test.ts} | 0 .../{template.test.js => template.test.ts} | 12 +- ...{encryption.test.js => encryption.test.ts} | 0 .../{endpoint.test.js => endpoint.test.ts} | 51 +++-- ....test.js => server-islands-render.test.ts} | 70 ++++++- ...red-state.test.js => shared-state.test.ts} | 0 ...-session.test.js => astro-session.test.ts} | 189 +++++++++++------- ...{controller.test.js => controller.test.ts} | 68 ++++--- .../{compile.test.js => compile.test.ts} | 56 ++++-- .../{hmr.test.js => hmr.test.ts} | 0 .../{escape.test.js => escape.test.ts} | 13 +- .../{slots.test.js => slots.test.ts} | 2 +- .../{transform.test.js => transform.test.ts} | 0 pnpm-lock.yaml | 9 - 34 files changed, 433 insertions(+), 278 deletions(-) delete mode 100644 packages/astro/test/units/_temp-fixtures/package.json rename packages/astro/test/units/actions/{action-error.test.js => action-error.test.ts} (91%) rename packages/astro/test/units/actions/{action-path.test.js => action-path.test.ts} (99%) rename packages/astro/test/units/actions/{actions-proxy.test.js => actions-proxy.test.ts} (84%) rename packages/astro/test/units/actions/{form-data-to-object.test.js => form-data-to-object.test.ts} (98%) rename packages/astro/test/units/actions/{serialize.test.js => serialize.test.ts} (95%) rename packages/astro/test/units/compile/{css-base-path.test.js => css-base-path.test.ts} (92%) rename packages/astro/test/units/compile/{invalid-css.test.js => invalid-css.test.ts} (82%) rename packages/astro/test/units/compile/{rust-compiler.test.js => rust-compiler.test.ts} (91%) rename packages/astro/test/units/config/{config-merge.test.js => config-merge.test.ts} (56%) rename packages/astro/test/units/config/{config-resolve.test.js => config-resolve.test.ts} (100%) rename packages/astro/test/units/config/{config-server.test.js => config-server.test.ts} (83%) rename packages/astro/test/units/config/{config-tsconfig.test.js => config-tsconfig.test.ts} (73%) rename packages/astro/test/units/config/{config-validate.test.js => config-validate.test.ts} (98%) rename packages/astro/test/units/content-collections/{get-entry-info.test.js => get-entry-info.test.ts} (100%) rename packages/astro/test/units/content-collections/{get-entry-type.test.js => get-entry-type.test.ts} (100%) rename packages/astro/test/units/content-collections/{image-references.test.js => image-references.test.ts} (87%) rename packages/astro/test/units/content-collections/{mutable-data-store.test.js => mutable-data-store.test.ts} (99%) rename packages/astro/test/units/middleware/{locals.test.js => locals.test.ts} (95%) rename packages/astro/test/units/redirects/{open-redirect.test.js => open-redirect.test.ts} (100%) rename packages/astro/test/units/redirects/{template.test.js => template.test.ts} (93%) rename packages/astro/test/units/server-islands/{encryption.test.js => encryption.test.ts} (100%) rename packages/astro/test/units/server-islands/{endpoint.test.js => endpoint.test.ts} (84%) rename packages/astro/test/units/server-islands/{server-islands-render.test.js => server-islands-render.test.ts} (86%) rename packages/astro/test/units/server-islands/{shared-state.test.js => shared-state.test.ts} (100%) rename packages/astro/test/units/sessions/{astro-session.test.js => astro-session.test.ts} (71%) rename packages/astro/test/units/vite-plugin-astro-server/{controller.test.js => controller.test.ts} (61%) rename packages/astro/test/units/vite-plugin-astro/{compile.test.js => compile.test.ts} (62%) rename packages/astro/test/units/vite-plugin-astro/{hmr.test.js => hmr.test.ts} (100%) rename packages/astro/test/units/vite-plugin-html/{escape.test.js => escape.test.ts} (93%) rename packages/astro/test/units/vite-plugin-html/{slots.test.js => slots.test.ts} (98%) rename packages/astro/test/units/vite-plugin-html/{transform.test.js => transform.test.ts} (100%) diff --git a/packages/astro/src/core/cookies/cookies.ts b/packages/astro/src/core/cookies/cookies.ts index e99d69443452..5ec231f56aa1 100644 --- a/packages/astro/src/core/cookies/cookies.ts +++ b/packages/astro/src/core/cookies/cookies.ts @@ -19,7 +19,7 @@ export interface AstroCookieGetOptions { decode?: (value: string) => string; } -type AstroCookieDeleteOptions = Omit; +export type AstroCookieDeleteOptions = Omit; interface AstroCookieInterface { value: string; diff --git a/packages/astro/test/units/_temp-fixtures/package.json b/packages/astro/test/units/_temp-fixtures/package.json deleted file mode 100644 index 3ecea0bfe38d..000000000000 --- a/packages/astro/test/units/_temp-fixtures/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "astro-temp-fixtures", - "description": "This directory contains nested directories of dynamically created unit test fixtures. The deps here can be used by them", - "dependencies": { - "@astrojs/mdx": "workspace:*", - "astro": "workspace:*" - } -} diff --git a/packages/astro/test/units/actions/action-error.test.js b/packages/astro/test/units/actions/action-error.test.ts similarity index 91% rename from packages/astro/test/units/actions/action-error.test.js rename to packages/astro/test/units/actions/action-error.test.ts index 5e506a3ddb0b..e0d8f5150563 100644 --- a/packages/astro/test/units/actions/action-error.test.js +++ b/packages/astro/test/units/actions/action-error.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { @@ -8,6 +7,7 @@ import { isActionError, isInputError, } from '../../../dist/actions/runtime/client.js'; +import type { ActionErrorCode } from '../../../dist/actions/runtime/types.js'; describe('ActionError', () => { it('sets code, status, and message from constructor', () => { @@ -42,7 +42,7 @@ describe('ActionError', () => { describe('ActionError.codeToStatus', () => { it('maps all known codes to correct HTTP status', () => { - for (const [code, status] of Object.entries(codeToStatusMap)) { + for (const [code, status] of Object.entries(codeToStatusMap) as [ActionErrorCode, number][]) { assert.equal(ActionError.codeToStatus(code), status, `Expected ${code} to map to ${status}`); } }); @@ -95,7 +95,7 @@ describe('ActionInputError', () => { { code: 'invalid_type', message: 'Expected string', path: ['name'] }, { code: 'too_small', message: 'Too short', path: ['name'] }, { code: 'invalid_type', message: 'Required', path: ['email'] }, - ]; + ] as unknown as ConstructorParameters[0]; const error = new ActionInputError(issues); assert.equal(error.code, 'BAD_REQUEST'); assert.equal(error.status, 400); @@ -111,7 +111,9 @@ describe('ActionInputError', () => { }); it('handles issues without paths', () => { - const issues = [{ code: 'custom', message: 'Something wrong', path: [] }]; + const issues = [ + { code: 'custom', message: 'Something wrong', path: [] }, + ] as unknown as ConstructorParameters[0]; const error = new ActionInputError(issues); assert.deepEqual(error.fields, {}); }); @@ -146,7 +148,7 @@ describe('isActionError', () => { describe('isInputError', () => { it('returns true for ActionInputError instances', () => { - const issues = [{ code: 'invalid_type', message: 'bad', path: ['x'] }]; + const issues = [{ code: 'invalid_type', message: 'bad', path: ['x'] }] as unknown as ConstructorParameters[0]; assert.equal(isInputError(new ActionInputError(issues)), true); }); diff --git a/packages/astro/test/units/actions/action-path.test.js b/packages/astro/test/units/actions/action-path.test.ts similarity index 99% rename from packages/astro/test/units/actions/action-path.test.js rename to packages/astro/test/units/actions/action-path.test.ts index 701cb375ba16..5e9c57133c3c 100644 --- a/packages/astro/test/units/actions/action-path.test.js +++ b/packages/astro/test/units/actions/action-path.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { diff --git a/packages/astro/test/units/actions/actions-proxy.test.js b/packages/astro/test/units/actions/actions-proxy.test.ts similarity index 84% rename from packages/astro/test/units/actions/actions-proxy.test.js rename to packages/astro/test/units/actions/actions-proxy.test.ts index 7b6f0917e2a7..a8fa5c4fabd7 100644 --- a/packages/astro/test/units/actions/actions-proxy.test.js +++ b/packages/astro/test/units/actions/actions-proxy.test.ts @@ -1,19 +1,26 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { APIContext } from '../../../dist/types/public/context.js'; +import type { SafeResult } from '../../../dist/actions/runtime/types.js'; import { createActionsProxy, ActionError } from '../../../dist/actions/runtime/client.js'; -/** - * Creates a proxy with a spy handleAction that records calls and returns a configurable result. - * @param {object} [opts] - * @param {import('../../../dist/actions/runtime/client.js').SafeResult} [opts.result] - */ -function setup(opts = {}) { - const result = opts.result ?? { data: 'ok', error: undefined }; - /** @type {{ param: any; path: string; context: any }[]} */ - const calls = []; - - const handleAction = async (param, path, context) => { +// #region Helpers + +interface SetupOptions { + result?: SafeResult; +} + +interface CallRecord { + param: unknown; + path: string; + context: APIContext | undefined; +} + +function setup(opts: SetupOptions = {}) { + const result: SafeResult = opts.result ?? { data: 'ok', error: undefined }; + const calls: CallRecord[] = []; + + const handleAction = async (param: unknown, path: string, context: APIContext | undefined) => { calls.push({ param, path, context }); return result; }; @@ -22,6 +29,10 @@ function setup(opts = {}) { return { proxy, calls }; } +// #endregion + +// #region Tests + describe('createActionsProxy', () => { describe('path building', () => { it('builds a top-level path from property access', async () => { @@ -121,3 +132,5 @@ describe('createActionsProxy', () => { }); }); }); + +// #endregion diff --git a/packages/astro/test/units/actions/form-data-to-object.test.js b/packages/astro/test/units/actions/form-data-to-object.test.ts similarity index 98% rename from packages/astro/test/units/actions/form-data-to-object.test.js rename to packages/astro/test/units/actions/form-data-to-object.test.ts index c3a2978615f9..71163a67080d 100644 --- a/packages/astro/test/units/actions/form-data-to-object.test.js +++ b/packages/astro/test/units/actions/form-data-to-object.test.ts @@ -40,14 +40,14 @@ describe('formDataToObject', () => { }); const res = formDataToObject(formData, input); - assert.ok(isNaN(res.age)); + assert.ok(isNaN(res.age as number)); }); it('should handle boolean checks', () => { const formData = new FormData(); formData.set('isCool', 'yes'); - formData.set('isTrue', true); - formData.set('isFalse', false); + formData.set('isTrue', String(true)); + formData.set('isFalse', String(false)); formData.set('falseString', 'false'); const input = z.object({ diff --git a/packages/astro/test/units/actions/serialize.test.js b/packages/astro/test/units/actions/serialize.test.ts similarity index 95% rename from packages/astro/test/units/actions/serialize.test.js rename to packages/astro/test/units/actions/serialize.test.ts index 853835379d68..3d7d5146a861 100644 --- a/packages/astro/test/units/actions/serialize.test.js +++ b/packages/astro/test/units/actions/serialize.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import * as devalue from 'devalue'; @@ -8,6 +7,7 @@ import { ActionInputError, deserializeActionResult, } from '../../../dist/actions/runtime/client.js'; +import type { ActionErrorCode } from '../../../dist/actions/runtime/types.js'; describe('serializeActionResult', () => { describe('data results', () => { @@ -89,7 +89,8 @@ describe('serializeActionResult', () => { const result = serializeActionResult({ data: undefined, error: undefined }); assert.equal(result.type, 'empty'); assert.equal(result.status, 204); - assert.equal(result.body, undefined); + // The 'empty' variant has no body field — verify it's absent at runtime + assert.equal('body' in result ? result.body : undefined, undefined); }); }); @@ -110,7 +111,7 @@ describe('serializeActionResult', () => { it('serializes an ActionInputError with issues and fields', () => { const issues = [ { code: 'invalid_type', expected: 'string', message: 'Required', path: ['comment'] }, - ]; + ] as unknown as ConstructorParameters[0]; const error = new ActionInputError(issues); const result = serializeActionResult({ data: undefined, error }); assert.equal(result.type, 'error'); @@ -125,7 +126,7 @@ describe('serializeActionResult', () => { }); it('uses correct status for different error codes', () => { - const codes = [ + const codes: [ActionErrorCode, number][] = [ ['BAD_REQUEST', 400], ['NOT_FOUND', 404], ['INTERNAL_SERVER_ERROR', 500], @@ -183,7 +184,7 @@ describe('deserializeActionResult', () => { it('deserializes an ActionInputError result', () => { const issues = [ { code: 'invalid_type', expected: 'string', message: 'Required', path: ['name'] }, - ]; + ] as unknown as ConstructorParameters[0]; const serialized = serializeActionResult({ data: undefined, error: new ActionInputError(issues), diff --git a/packages/astro/test/units/compile/css-base-path.test.js b/packages/astro/test/units/compile/css-base-path.test.ts similarity index 92% rename from packages/astro/test/units/compile/css-base-path.test.js rename to packages/astro/test/units/compile/css-base-path.test.ts index 8e5638fa1f97..065f1f14364e 100644 --- a/packages/astro/test/units/compile/css-base-path.test.js +++ b/packages/astro/test/units/compile/css-base-path.test.ts @@ -3,41 +3,34 @@ import { describe, it } from 'node:test'; import { pathToFileURL } from 'node:url'; import { resolveConfig } from 'vite'; import { compileAstro } from '../../../dist/vite-plugin-astro/compile.js'; +import type { AstroConfig } from '../../../dist/types/public/config.js'; +import type { CompileProps } from '../../../dist/core/compile/compile.js'; +import { Logger } from '../../../dist/core/logger/core.js'; +import { nodeLogDestination } from '../../../dist/core/logger/node.js'; -/** - * Compile Astro source with a given base path - * @param {string} source - Astro source code - * @param {string} base - Base path configuration - */ -async function compileWithBase(source, base = '/') { +const logger = new Logger({ dest: nodeLogDestination, level: 'silent' }); + +/** Compile Astro source with a given base path. */ +async function compileWithBase(source: string, base = '/') { const viteConfig = await resolveConfig({ configFile: false }, 'serve'); - const result = await compileAstro({ - compileProps: { - astroConfig: { - root: pathToFileURL('/'), - base, - experimental: {}, - build: { - format: 'directory', - }, - trailingSlash: 'ignore', - }, - viteConfig, - preferences: { - get: () => Promise.resolve(false), - }, - filename: '/src/pages/index.astro', - source, - }, + const props: CompileProps = { + astroConfig: { + root: pathToFileURL('/'), + base, + experimental: {}, + build: { format: 'directory' }, + trailingSlash: 'ignore', + } as AstroConfig, + viteConfig, + toolbarEnabled: false, + filename: '/src/pages/index.astro', + source, + }; + return compileAstro({ + compileProps: props as any, astroFileToCompileMetadata: new Map(), - logger: { - info: () => {}, - warn: () => {}, - error: () => {}, - debug: () => {}, - }, + logger, }); - return result; } describe('CSS Base Path Rewriting', () => { diff --git a/packages/astro/test/units/compile/invalid-css.test.js b/packages/astro/test/units/compile/invalid-css.test.ts similarity index 82% rename from packages/astro/test/units/compile/invalid-css.test.js rename to packages/astro/test/units/compile/invalid-css.test.ts index 73d52e5ec8c1..9c3e0d043381 100644 --- a/packages/astro/test/units/compile/invalid-css.test.js +++ b/packages/astro/test/units/compile/invalid-css.test.ts @@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url'; import { resolveConfig } from 'vite'; import { compile } from '../../../dist/core/compile/index.js'; import { AggregateError } from '../../../dist/core/errors/index.js'; +import type { AstroConfig } from '../../../dist/types/public/config.js'; describe('astro/src/core/compile', () => { describe('Invalid CSS', () => { @@ -14,8 +15,9 @@ describe('astro/src/core/compile', () => { astroConfig: { root: pathToFileURL('/'), experimental: {}, - }, + } as AstroConfig, viteConfig: await resolveConfig({ configFile: false }, 'serve'), + toolbarEnabled: false, filename: '/src/pages/index.astro', source: ` --- @@ -37,7 +39,7 @@ describe('astro/src/core/compile', () => { } assert.equal(error instanceof AggregateError, true); - assert.equal(error.errors[0].message.includes('expected ")"'), true); + assert.equal((error as AggregateError).errors[0].message.includes('expected ")"'), true); }); }); }); diff --git a/packages/astro/test/units/compile/rust-compiler.test.js b/packages/astro/test/units/compile/rust-compiler.test.ts similarity index 91% rename from packages/astro/test/units/compile/rust-compiler.test.js rename to packages/astro/test/units/compile/rust-compiler.test.ts index aaa5bbe6fe68..0c53e68d7823 100644 --- a/packages/astro/test/units/compile/rust-compiler.test.js +++ b/packages/astro/test/units/compile/rust-compiler.test.ts @@ -3,12 +3,9 @@ import { describe, it } from 'node:test'; import { pathToFileURL } from 'node:url'; import { resolveConfig } from 'vite'; import { compile } from '../../../dist/core/compile/compile-rs.js'; +import type { AstroConfig } from '../../../dist/types/public/config.js'; -/** - * @param {string} source - * @param {object} [configOverrides] - */ -async function compileWithRust(source, configOverrides = {}) { +async function compileWithRust(source: string, configOverrides: Partial = {}) { const viteConfig = await resolveConfig({ configFile: false }, 'serve'); return compile({ astroConfig: { @@ -20,7 +17,7 @@ async function compileWithRust(source, configOverrides = {}) { devToolbar: { enabled: false }, site: undefined, ...configOverrides, - }, + } as AstroConfig, viteConfig, toolbarEnabled: false, filename: '/src/components/index.astro', @@ -130,9 +127,10 @@ console.log('hello'); it('throws a CompilerError on unclosed tags', async () => { await assert.rejects( () => compileWithRust('

Unclosed tag'), - (err) => { - assert.ok(err.message || err.name); - assert.ok(err.message.includes('Unexpected token')); + (err: unknown) => { + const e = err as { message?: string; name?: string }; + assert.ok(e.message || e.name); + assert.ok(e.message?.includes('Unexpected token')); return true; }, ); diff --git a/packages/astro/test/units/config/config-merge.test.js b/packages/astro/test/units/config/config-merge.test.ts similarity index 56% rename from packages/astro/test/units/config/config-merge.test.js rename to packages/astro/test/units/config/config-merge.test.ts index e269b454a0c8..59eba321d2da 100644 --- a/packages/astro/test/units/config/config-merge.test.js +++ b/packages/astro/test/units/config/config-merge.test.ts @@ -6,15 +6,17 @@ describe('mergeConfig', () => { it('keeps server.allowedHosts as boolean', () => { const defaults = { server: { - allowedHosts: [], + // Typed as string[] to match AstroConfig's allowedHosts field + allowedHosts: [] as string[], }, }; + // allowedHosts can also be true (allow all) — cast to satisfy DeepPartial const overrides = { server: { - allowedHosts: true, + allowedHosts: true as boolean | string[], }, }; - const merged = mergeConfig(defaults, overrides); + const merged = mergeConfig(defaults, overrides as typeof defaults); assert.equal(merged.server.allowedHosts, true); }); }); diff --git a/packages/astro/test/units/config/config-resolve.test.js b/packages/astro/test/units/config/config-resolve.test.ts similarity index 100% rename from packages/astro/test/units/config/config-resolve.test.js rename to packages/astro/test/units/config/config-resolve.test.ts diff --git a/packages/astro/test/units/config/config-server.test.js b/packages/astro/test/units/config/config-server.test.ts similarity index 83% rename from packages/astro/test/units/config/config-server.test.js rename to packages/astro/test/units/config/config-server.test.ts index 6f621007c14a..ab9c0d6b83c6 100644 --- a/packages/astro/test/units/config/config-server.test.js +++ b/packages/astro/test/units/config/config-server.test.ts @@ -1,20 +1,12 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; -import { flagsToAstroInlineConfig } from '../../../dist/cli/flags.js'; +import { flagsToAstroInlineConfig, type Flags } from '../../../dist/cli/flags.js'; import { resolveConfig } from '../../../dist/core/config/index.js'; -const cwd = fileURLToPath(new URL('../../fixtures/config-host/', import.meta.url)); - describe('config.server', () => { - function resolveConfigWithFlags(flags) { - return resolveConfig( - flagsToAstroInlineConfig({ - root: cwd, - ...flags, - }), - 'dev', - ); + function resolveConfigWithFlags(flags: Partial) { + return resolveConfig(flagsToAstroInlineConfig(flags as Flags), 'dev'); } describe('host', () => { @@ -64,8 +56,8 @@ describe('config.server', () => { config: configFileURL, }); assert.equal(false, true, 'this should not have resolved'); - } catch (err) { - assert.equal(err.message.includes('Unable to resolve'), true); + } catch (err: unknown) { + assert.equal((err as Error).message.includes('Unable to resolve'), true); } }); }); diff --git a/packages/astro/test/units/config/config-tsconfig.test.js b/packages/astro/test/units/config/config-tsconfig.test.ts similarity index 73% rename from packages/astro/test/units/config/config-tsconfig.test.js rename to packages/astro/test/units/config/config-tsconfig.test.ts index 94e9438982fd..218256d4e405 100644 --- a/packages/astro/test/units/config/config-tsconfig.test.js +++ b/packages/astro/test/units/config/config-tsconfig.test.ts @@ -5,29 +5,37 @@ import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; import { toJson } from 'tsconfck'; import { loadTSConfig, updateTSConfigForFramework } from '../../../dist/core/config/index.js'; +import type { frameworkWithTSSettings } from '../../../dist/core/config/tsconfig.js'; const cwd = fileURLToPath(new URL('../../fixtures/tsconfig-handling/', import.meta.url)); +/** Assert that loadTSConfig returned a valid result (not an error string). */ +function assertValidConfig( + config: Awaited>, +): asserts config is Exclude { + assert.ok( + typeof config !== 'string', + `Expected a valid config but got error: ${config}`, + ); +} + describe('TSConfig handling', () => { describe('tsconfig / jsconfig loading', () => { it('can load tsconfig.json', async () => { const config = await loadTSConfig(cwd); - assert.equal(config !== undefined, true); }); it('can resolve tsconfig.json up directories', async () => { const config = await loadTSConfig(cwd); - - assert.equal(config !== undefined, true); + assertValidConfig(config); assert.equal(config.tsconfigFile, path.join(cwd, 'tsconfig.json')); assert.deepEqual(config.tsconfig.files, ['im-a-test']); }); it('can fall back to jsconfig.json if tsconfig.json does not exist', async () => { const config = await loadTSConfig(path.join(cwd, 'jsconfig')); - - assert.equal(config !== undefined, true); + assertValidConfig(config); assert.equal(config.tsconfigFile, path.join(cwd, 'jsconfig', 'jsconfig.json')); assert.deepEqual(config.tsconfig.files, ['im-a-test-js']); }); @@ -42,6 +50,7 @@ describe('TSConfig handling', () => { it('does not change baseUrl in raw config', async () => { const loadedConfig = await loadTSConfig(path.join(cwd, 'baseUrl')); + assertValidConfig(loadedConfig); const rawConfig = await readFile(path.join(cwd, 'baseUrl', 'tsconfig.json'), 'utf-8') .then(toJson) .then((content) => JSON.parse(content)); @@ -53,15 +62,21 @@ describe('TSConfig handling', () => { describe('tsconfig / jsconfig updates', () => { it('can update a tsconfig with a framework config', async () => { const config = await loadTSConfig(cwd); + assertValidConfig(config); const updatedConfig = updateTSConfigForFramework(config.tsconfig, 'react'); assert.notEqual(config.tsconfig, 'react-jsx'); - assert.equal(updatedConfig.compilerOptions.jsx, 'react-jsx'); + assert.equal(updatedConfig.compilerOptions?.jsx, 'react-jsx'); }); it('produce no changes on invalid frameworks', async () => { const config = await loadTSConfig(cwd); - const updatedConfig = updateTSConfigForFramework(config.tsconfig, 'doesnt-exist'); + assertValidConfig(config); + // 'doesnt-exist' is not a valid frameworkWithTSSettings — cast to test fallback behaviour + const updatedConfig = updateTSConfigForFramework( + config.tsconfig, + 'doesnt-exist' as frameworkWithTSSettings, + ); assert.deepEqual(config.tsconfig, updatedConfig); }); diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.ts similarity index 98% rename from packages/astro/test/units/config/config-validate.test.js rename to packages/astro/test/units/config/config-validate.test.ts index 8938c14e166d..d70d884c5ba7 100644 --- a/packages/astro/test/units/config/config-validate.test.js +++ b/packages/astro/test/units/config/config-validate.test.ts @@ -1,4 +1,3 @@ -// @ts-check import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { stripVTControlCharacters } from 'node:util'; @@ -9,11 +8,7 @@ import { validateConfig as _validateConfig } from '../../../dist/core/config/val import { formatConfigErrorMessage } from '../../../dist/core/messages/runtime.js'; import { envField } from '../../../dist/env/config.js'; -/** - * - * @param {any} userConfig - */ -async function validateConfig(userConfig) { +async function validateConfig(userConfig: Record) { return _validateConfig(userConfig, process.cwd(), ''); } @@ -544,8 +539,8 @@ describe('Config Validation', () => { ttl: 60 * 60, // 1 hour }, }); - assert.equal(result.session.ttl, 60 * 60); - assert.equal(result.session.driver, undefined); + assert.equal(result.session?.ttl, 60 * 60); + assert.equal(result.session?.driver, undefined); }); }); diff --git a/packages/astro/test/units/content-collections/get-entry-info.test.js b/packages/astro/test/units/content-collections/get-entry-info.test.ts similarity index 100% rename from packages/astro/test/units/content-collections/get-entry-info.test.js rename to packages/astro/test/units/content-collections/get-entry-info.test.ts diff --git a/packages/astro/test/units/content-collections/get-entry-type.test.js b/packages/astro/test/units/content-collections/get-entry-type.test.ts similarity index 100% rename from packages/astro/test/units/content-collections/get-entry-type.test.js rename to packages/astro/test/units/content-collections/get-entry-type.test.ts diff --git a/packages/astro/test/units/content-collections/image-references.test.js b/packages/astro/test/units/content-collections/image-references.test.ts similarity index 87% rename from packages/astro/test/units/content-collections/image-references.test.js rename to packages/astro/test/units/content-collections/image-references.test.ts index 84595e0cee58..436f68288157 100644 --- a/packages/astro/test/units/content-collections/image-references.test.js +++ b/packages/astro/test/units/content-collections/image-references.test.ts @@ -1,18 +1,19 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { updateImageReferencesInData } from '../../../dist/content/runtime.js'; import { imageSrcToImportId } from '../../../dist/assets/utils/resolveImports.js'; +import type { ImageMetadata } from '../../../dist/assets/types.js'; const IMAGE_PREFIX = '__ASTRO_IMAGE_'; const FILE_NAME = 'src/content/blog/post.md'; -function makeImageMap(src, meta) { +function makeImageMap(src: string, meta: ImageMetadata): Map { const id = imageSrcToImportId(src, FILE_NAME); + assert.ok(id, `imageSrcToImportId returned undefined for src="${src}"`); return new Map([[id, meta]]); } -const heroMeta = { +const heroMeta: ImageMetadata = { src: '/_astro/hero.abc123.png', width: 800, height: 600, @@ -76,10 +77,12 @@ describe('updateImageReferencesInData', () => { }); it('resolves multiple different images in the same entry', () => { - const thumbMeta = { src: '/_astro/thumb.xyz.png', width: 100, height: 100, format: 'png' }; + const thumbMeta: ImageMetadata = { src: '/_astro/thumb.xyz.png', width: 100, height: 100, format: 'png' }; const heroId = imageSrcToImportId('./hero.png', FILE_NAME); const thumbId = imageSrcToImportId('./thumb.png', FILE_NAME); - const map = new Map([ + assert.ok(heroId); + assert.ok(thumbId); + const map = new Map([ [heroId, heroMeta], [thumbId, thumbMeta], ]); diff --git a/packages/astro/test/units/content-collections/mutable-data-store.test.js b/packages/astro/test/units/content-collections/mutable-data-store.test.ts similarity index 99% rename from packages/astro/test/units/content-collections/mutable-data-store.test.js rename to packages/astro/test/units/content-collections/mutable-data-store.test.ts index e17db611eb20..ce27408b838b 100644 --- a/packages/astro/test/units/content-collections/mutable-data-store.test.js +++ b/packages/astro/test/units/content-collections/mutable-data-store.test.ts @@ -10,7 +10,7 @@ import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; import { imageSrcToImportId } from '../../../dist/assets/utils/resolveImports.js'; describe('MutableDataStore', () => { - let tmpDir; + let tmpDir: string; before(async () => { tmpDir = await mkdtemp(path.join(tmpdir(), 'astro-test-')); diff --git a/packages/astro/test/units/middleware/locals.test.js b/packages/astro/test/units/middleware/locals.test.ts similarity index 95% rename from packages/astro/test/units/middleware/locals.test.js rename to packages/astro/test/units/middleware/locals.test.ts index eada9afbadbb..1a896a18f4db 100644 --- a/packages/astro/test/units/middleware/locals.test.js +++ b/packages/astro/test/units/middleware/locals.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { isLocalsSerializable, trySerializeLocals } from '../../../dist/core/middleware/index.js'; @@ -56,8 +55,9 @@ describe('isLocalsSerializable', () => { it('handles deeply nested objects without stack overflow (iterative implementation)', () => { // Build a 10,000-level deep object — would overflow the call stack with recursion - let deep = /** @type {any} */ ({}); - let current = deep; + type DeepObject = { child?: DeepObject; value?: string }; + const deep: DeepObject = {}; + let current: DeepObject = deep; for (let i = 0; i < 10_000; i++) { current.child = {}; current = current.child; diff --git a/packages/astro/test/units/redirects/open-redirect.test.js b/packages/astro/test/units/redirects/open-redirect.test.ts similarity index 100% rename from packages/astro/test/units/redirects/open-redirect.test.js rename to packages/astro/test/units/redirects/open-redirect.test.ts diff --git a/packages/astro/test/units/redirects/template.test.js b/packages/astro/test/units/redirects/template.test.ts similarity index 93% rename from packages/astro/test/units/redirects/template.test.js rename to packages/astro/test/units/redirects/template.test.ts index 26da2255a157..05a4786bca7b 100644 --- a/packages/astro/test/units/redirects/template.test.js +++ b/packages/astro/test/units/redirects/template.test.ts @@ -38,8 +38,8 @@ describe('redirects/template', () => { const link = $('body a'); assert.equal(link.length, 1); assert.equal(link.attr('href'), '/new-page'); - assert.ok(link.html().includes('Redirecting')); - assert.ok(link.html().includes('/new-page')); + assert.ok(link.html()?.includes('Redirecting')); + assert.ok(link.html()?.includes('/new-page')); }); it('uses 2 second delay for 302 redirects', () => { @@ -88,8 +88,8 @@ describe('redirects/template', () => { const $ = cheerio.load(html); const bodyText = $('body').html(); - assert.ok(bodyText.includes('from /old')); - assert.ok(bodyText.includes('to /new')); + assert.ok(bodyText?.includes('from /old')); + assert.ok(bodyText?.includes('to /new')); }); it('omits "from" text when not provided', () => { @@ -101,8 +101,8 @@ describe('redirects/template', () => { const $ = cheerio.load(html); const bodyText = $('body').html(); - assert.ok(!bodyText.includes('from ')); - assert.ok(bodyText.includes('to /new')); + assert.ok(!bodyText?.includes('from ')); + assert.ok(bodyText?.includes('to /new')); }); it('handles special characters in URLs', () => { diff --git a/packages/astro/test/units/server-islands/encryption.test.js b/packages/astro/test/units/server-islands/encryption.test.ts similarity index 100% rename from packages/astro/test/units/server-islands/encryption.test.js rename to packages/astro/test/units/server-islands/encryption.test.ts diff --git a/packages/astro/test/units/server-islands/endpoint.test.js b/packages/astro/test/units/server-islands/endpoint.test.ts similarity index 84% rename from packages/astro/test/units/server-islands/endpoint.test.js rename to packages/astro/test/units/server-islands/endpoint.test.ts index 3cbe5abca746..5592635949d1 100644 --- a/packages/astro/test/units/server-islands/endpoint.test.js +++ b/packages/astro/test/units/server-islands/endpoint.test.ts @@ -1,13 +1,18 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { getRequestData } from '../../../dist/core/server-islands/endpoint.js'; +import type { RenderOptions } from '../../../dist/core/server-islands/endpoint.js'; // #region Helpers +function isRenderOptions(result: Response | RenderOptions): result is RenderOptions { + return !(result instanceof Response); +} + /** * Construct a minimal Request for testing getRequestData. */ -function makeGetRequest(params = {}) { +function makeGetRequest(params: Record = {}) { const url = new URL('http://localhost/_server-islands/Island'); for (const [key, value] of Object.entries(params)) { url.searchParams.set(key, value); @@ -15,7 +20,7 @@ function makeGetRequest(params = {}) { return new Request(url.toString(), { method: 'GET' }); } -function makePostRequest(body) { +function makePostRequest(body: RenderOptions) { return new Request('http://localhost/_server-islands/Island', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -23,7 +28,19 @@ function makePostRequest(body) { }); } -function makeMethodRequest(method) { +/** + * Like makePostRequest but accepts any payload — used to test server-side + * validation of intentionally malformed or invalid request bodies. + */ +function makeInvalidPostRequest(body: Record) { + return new Request('http://localhost/_server-islands/Island', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +function makeMethodRequest(method: string = 'GET') { return new Request('http://localhost/_server-islands/Island', { method }); } @@ -35,7 +52,7 @@ describe('getRequestData', () => { it('returns RenderOptions when all required params are present', async () => { const req = makeGetRequest({ s: 'slots', e: 'export', p: 'props' }); const result = await getRequestData(req); - assert.ok(!(result instanceof Response), 'should not return a Response'); + assert.ok(isRenderOptions(result), 'should not return a Response'); assert.equal(result.encryptedSlots, 'slots'); assert.equal(result.encryptedComponentExport, 'export'); assert.equal(result.encryptedProps, 'props'); @@ -72,7 +89,7 @@ describe('getRequestData', () => { it('accepts empty-string param values (empty props / slots are valid)', async () => { const req = makeGetRequest({ s: '', e: 'export', p: '' }); const result = await getRequestData(req); - assert.ok(!(result instanceof Response), 'should not return a Response'); + assert.ok(isRenderOptions(result), 'should not return a Response'); assert.equal(result.encryptedSlots, ''); assert.equal(result.encryptedProps, ''); }); @@ -88,14 +105,14 @@ describe('getRequestData', () => { encryptedSlots: 'encSlots', }); const result = await getRequestData(req); - assert.ok(!(result instanceof Response), 'should not return a Response'); + assert.ok(isRenderOptions(result), 'should not return a Response'); assert.equal(result.encryptedComponentExport, 'encExport'); assert.equal(result.encryptedProps, 'encProps'); assert.equal(result.encryptedSlots, 'encSlots'); }); it('returns 400 when POST body contains plaintext `slots` object', async () => { - const req = makePostRequest({ + const req = makeInvalidPostRequest({ encryptedComponentExport: 'encExport', encryptedProps: '', slots: { default: '

Hello

' }, @@ -110,7 +127,7 @@ describe('getRequestData', () => { }); it('returns 400 when POST body contains plaintext `componentExport` string', async () => { - const req = makePostRequest({ + const req = makeInvalidPostRequest({ componentExport: 'default', encryptedProps: '', encryptedSlots: '', @@ -142,14 +159,14 @@ describe('getRequestData', () => { encryptedSlots: '', }); const result = await getRequestData(req); - assert.ok(!(result instanceof Response), 'should not return a Response'); + assert.ok(isRenderOptions(result), 'should not return a Response'); assert.equal(result.encryptedProps, ''); assert.equal(result.encryptedSlots, ''); }); it('only checks own properties for `slots` validation', async () => { // Temporarily pollute Object.prototype to simulate inherited properties - Object.prototype.slots = { default: 'polluted' }; + (Object.prototype as any).slots = { default: 'polluted' }; try { const req = makePostRequest({ encryptedComponentExport: 'encExport', @@ -158,17 +175,17 @@ describe('getRequestData', () => { }); const result = await getRequestData(req); assert.ok( - !(result instanceof Response), + isRenderOptions(result), `Expected RenderOptions but got Response with status ${result instanceof Response ? result.status : 'N/A'} — inherited 'slots' should not trigger rejection`, ); } finally { - delete Object.prototype.slots; + delete (Object.prototype as any).slots; } }); it('only checks own properties for `componentExport` validation', async () => { // Temporarily pollute Object.prototype to simulate inherited properties - Object.prototype.componentExport = 'default'; + (Object.prototype as any).componentExport = 'default'; try { const req = makePostRequest({ encryptedComponentExport: 'encExport', @@ -177,11 +194,11 @@ describe('getRequestData', () => { }); const result = await getRequestData(req); assert.ok( - !(result instanceof Response), + isRenderOptions(result), `Expected RenderOptions but got Response with status ${result instanceof Response ? result.status : 'N/A'} — inherited 'componentExport' should not trigger rejection`, ); } finally { - delete Object.prototype.componentExport; + delete (Object.prototype as any).componentExport; } }); }); @@ -240,7 +257,7 @@ describe('getRequestData', () => { body: JSON.stringify(body), }); const result = await getRequestData(req, limit); - assert.ok(!(result instanceof Response), 'should not return a Response'); + assert.ok(isRenderOptions(result), 'should not return a Response'); assert.equal(result.encryptedComponentExport, 'encExport'); }); @@ -253,7 +270,7 @@ describe('getRequestData', () => { }; const req = makePostRequest(body); const result = await getRequestData(req); - assert.ok(!(result instanceof Response), 'should not return a Response'); + assert.ok(isRenderOptions(result), 'should not return a Response'); assert.equal(result.encryptedComponentExport, 'encExport'); }); }); diff --git a/packages/astro/test/units/server-islands/server-islands-render.test.js b/packages/astro/test/units/server-islands/server-islands-render.test.ts similarity index 86% rename from packages/astro/test/units/server-islands/server-islands-render.test.js rename to packages/astro/test/units/server-islands/server-islands-render.test.ts index 97880a7a5796..ede17db30038 100644 --- a/packages/astro/test/units/server-islands/server-islands-render.test.js +++ b/packages/astro/test/units/server-islands/server-islands-render.test.ts @@ -6,27 +6,74 @@ import { containsServerDirective, renderServerIslandRuntime, } from '../../../dist/runtime/server/render/server-islands.js'; +import type { SSRResult } from '../../../dist/types/public/internal.js'; +import type { + RenderDestination, + RenderDestinationChunk, +} from '../../../dist/runtime/server/render/common.js'; +import type { ComponentSlotValue } from '../../../dist/runtime/server/render/slot.js'; +import { renderTemplate } from '../../../dist/runtime/server/index.js'; // #region Helpers -/** Minimal SSRResult stub sufficient for ServerIslandComponent. */ -async function createStubResult(overrides = {}) { +/** + * Minimal SSRResult stub sufficient for ServerIslandComponent. + * + * TODO: Replace with a shared `createMockResult()` helper once + * the unit test suite is fully migrated to TypeScript. + */ +async function createStubResult(overrides: Partial = {}): Promise { const key = await createKey(); return { - key: Promise.resolve(key), + cancelled: false, + base: '/', + userAssetsBase: undefined, + styles: new Set(), + scripts: new Set(), + links: new Set(), + componentMetadata: new Map(), + inlinedScripts: new Map(), + createAstro() { + throw new Error('createAstro() not available in unit tests'); + }, + params: {}, + resolve: async (s: string) => s, + response: { status: 200, statusText: 'OK', headers: new Headers() }, + request: new Request('http://localhost/'), + renderers: [], + clientDirectives: new Map(), + compressHTML: false, + partial: false, + pathname: '/', + cookies: undefined, serverIslandNameMap: new Map([ ['src/components/Island.astro', 'Island'], ['src/components/BigIsland.astro', 'BigIsland'], ]), - base: '/', trailingSlash: 'never', + key: Promise.resolve(key), _metadata: { + hasHydrationScript: false, + rendererSpecificHydrationScripts: new Set(), + hasRenderedHead: false, + renderedScripts: new Set(), + hasDirectives: new Set(), + hasRenderedServerIslandRuntime: false, + headInTree: false, extraHead: [], + extraStyleHashes: [], extraScriptHashes: [], - hasRenderedServerIslandRuntime: false, propagators: new Set(), }, - cspDestination: undefined, + cspDestination: 'header', + shouldInjectCspMetaTags: false, + cspAlgorithm: 'SHA-256', + scriptHashes: [], + scriptResources: [], + styleHashes: [], + styleResources: [], + directives: [], + isStrictDynamic: false, internalFetchHeaders: {}, ...overrides, }; @@ -34,9 +81,9 @@ async function createStubResult(overrides = {}) { /** Collect all chunks written to a destination into a single string. */ function createDestination() { - const chunks = []; - const destination = { - write(chunk) { + const chunks: RenderDestinationChunk[] = []; + const destination: RenderDestination = { + write(chunk: RenderDestinationChunk) { chunks.push(chunk); }, }; @@ -273,7 +320,7 @@ describe('ServerIslandComponent', () => { it('renders fallback slot content inline', async () => { const result = await createStubResult(); // The fallback slot is a function that returns a renderable value - const fallbackSlot = () => 'Loading...'; + const fallbackSlot: ComponentSlotValue = () => renderTemplate`Loading...`; const component = new ServerIslandComponent( result, islandProps(), @@ -293,7 +340,8 @@ describe('ServerIslandComponent', () => { const result = await createStubResult(); // A non-fallback slot called "content" — its HTML should NOT appear directly in render() // output; instead it is encrypted and sent to the island endpoint. - const contentSlot = () => 'Slot content that should be encrypted'; + const contentSlot: ComponentSlotValue = () => + renderTemplate`Slot content that should be encrypted`; const component = new ServerIslandComponent( result, islandProps(), diff --git a/packages/astro/test/units/server-islands/shared-state.test.js b/packages/astro/test/units/server-islands/shared-state.test.ts similarity index 100% rename from packages/astro/test/units/server-islands/shared-state.test.js rename to packages/astro/test/units/server-islands/shared-state.test.ts diff --git a/packages/astro/test/units/sessions/astro-session.test.js b/packages/astro/test/units/sessions/astro-session.test.ts similarity index 71% rename from packages/astro/test/units/sessions/astro-session.test.js rename to packages/astro/test/units/sessions/astro-session.test.ts index 22457ef49d9c..654359c3ae62 100644 --- a/packages/astro/test/units/sessions/astro-session.test.js +++ b/packages/astro/test/units/sessions/astro-session.test.ts @@ -2,39 +2,59 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { stringify as devalueStringify } from 'devalue'; import driverFactory from 'unstorage/drivers/memory'; +import type { Storage } from 'unstorage'; import { AstroSession, PERSIST_SYMBOL } from '../../../dist/core/session/runtime.js'; +import type { SSRManifestSession } from '../../../dist/core/app/types.js'; +import type { RuntimeMode } from '../../../dist/types/public/config.js'; +import type { + AstroCookieSetOptions, + AstroCookieDeleteOptions, +} from '../../../dist/core/cookies/cookies.js'; +import type { SessionDriverFactory } from '../../../dist/core/session/types.js'; + +// #region Helpers + +/** Minimal cookie interface used by AstroSession. */ +interface MockCookies { + set(key: string, value: string, options?: AstroCookieSetOptions): void; + delete(key: string, options?: AstroCookieDeleteOptions): void; + get(key: string): { value: string } | undefined; +} -// Mock dependencies -const defaultMockCookies = { +const defaultMockCookies: MockCookies = { set: () => {}, delete: () => {}, get: () => ({ value: 'sessionid' }), }; -const stringify = (data) => JSON.parse(devalueStringify(data)); +const stringify = (data: unknown) => JSON.parse(devalueStringify(data)); -const defaultConfig = { +const defaultConfig: SSRManifestSession = { driver: 'memory', cookie: 'test-session', ttl: 60, }; -// Helper to create a new session instance with mocked dependencies function createSession( - config = defaultConfig, - cookies = defaultMockCookies, - mockStorage, - runtimeMode = 'production', + config: SSRManifestSession = defaultConfig, + cookies: MockCookies = defaultMockCookies, + mockStorage: Storage | null = null, + runtimeMode: RuntimeMode = 'production', ) { + // driverFactory from unstorage/drivers/memory accepts no config; wrap it to satisfy SessionDriverFactory + const typedDriverFactory: SessionDriverFactory = () => driverFactory(); return new AstroSession({ - cookies, + cookies: cookies as any, // MockCookies satisfies the methods AstroSession uses; AstroCookies has private fields config, runtimeMode, - driverFactory, + driverFactory: typedDriverFactory, mockStorage, }); } +// #endregion + +// #region Basic Operations describe('AstroSession - Basic Operations', () => { it('should set and get a value', async () => { const session = createSession(); @@ -77,10 +97,13 @@ describe('AstroSession - Basic Operations', () => { }); }); +// #endregion + +// #region Cookie Management describe('AstroSession - Cookie Management', () => { it('should set cookie on first value set', async () => { let cookieSet = false; - const mockCookies = { + const mockCookies: MockCookies = { ...defaultMockCookies, set: () => { cookieSet = true; @@ -94,11 +117,11 @@ describe('AstroSession - Cookie Management', () => { }); it('should delete cookie on destroy', async () => { - let cookieDeletedArgs; - let cookieDeletedName; - const mockCookies = { + let cookieDeletedArgs: AstroCookieDeleteOptions | undefined; + let cookieDeletedName: string | undefined; + const mockCookies: MockCookies = { ...defaultMockCookies, - delete: (name, args) => { + delete: (name: string, args?: AstroCookieDeleteOptions) => { cookieDeletedName = name; cookieDeletedArgs = args; }, @@ -111,6 +134,9 @@ describe('AstroSession - Cookie Management', () => { }); }); +// #endregion + +// #region Session Regeneration describe('AstroSession - Session Regeneration', () => { it('should preserve data when regenerating session', async () => { const session = createSession(); @@ -133,19 +159,19 @@ describe('AstroSession - Session Regeneration', () => { }); it('should persist data after regeneration without a subsequent set()', async () => { - const store = new Map(); + const store = new Map(); const mockStorage = { - get: async (key) => { + get: async (key: string) => { const raw = store.get(key); return raw ? JSON.parse(raw) : null; }, - setItem: async (key, value) => { + setItem: async (key: string, value: string) => { store.set(key, value); }, - removeItem: async (key) => { + removeItem: async (key: string) => { store.delete(key); }, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); @@ -160,7 +186,7 @@ describe('AstroSession - Session Regeneration', () => { defaultConfig, { ...defaultMockCookies, - get: () => ({ value: session.sessionID }), + get: () => ({ value: String(session.sessionID) }), }, mockStorage, ); @@ -170,15 +196,18 @@ describe('AstroSession - Session Regeneration', () => { }); }); +// #endregion + +// #region Data Persistence describe('AstroSession - Data Persistence', () => { it('should persist data to storage', async () => { - let storedData; + let storedData: string | undefined; const mockStorage = { get: async () => null, - setItem: async (_key, value) => { + setItem: async (_key: string, value: string) => { storedData = value; }, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); @@ -192,7 +221,7 @@ describe('AstroSession - Data Persistence', () => { const mockStorage = { get: async () => stringify(new Map([['key', { data: 'value' }]])), setItem: async () => {}, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); @@ -204,7 +233,7 @@ describe('AstroSession - Data Persistence', () => { const mockStorage = { get: async () => stringify(new Map([['key', { data: 'value', expires: -1 }]])), setItem: async () => {}, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); @@ -214,6 +243,9 @@ describe('AstroSession - Data Persistence', () => { }); }); +// #endregion + +// #region Error Handling describe('AstroSession - Error Handling', () => { it('should throw error when setting invalid data', async () => { const session = createSession(); @@ -231,7 +263,7 @@ describe('AstroSession - Error Handling', () => { const mockStorage = { get: async () => 'invalid-json', setItem: async () => {}, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); @@ -239,12 +271,15 @@ describe('AstroSession - Error Handling', () => { }); }); +// #endregion + +// #region Configuration describe('AstroSession - Configuration', () => { it('should use custom cookie name from config', async () => { - let cookieName; - const mockCookies = { + let cookieName: string | undefined; + const mockCookies: MockCookies = { ...defaultMockCookies, - set: (name) => { + set: (name: string) => { cookieName = name; }, }; @@ -262,10 +297,10 @@ describe('AstroSession - Configuration', () => { }); it('should use default cookie name if not specified', async () => { - let cookieName; - const mockCookies = { + let cookieName: string | undefined; + const mockCookies: MockCookies = { ...defaultMockCookies, - set: (name) => { + set: (name: string) => { cookieName = name; }, }; @@ -273,7 +308,7 @@ describe('AstroSession - Configuration', () => { const session = createSession( { ...defaultConfig, - // @ts-ignore + // @ts-ignore — intentionally testing undefined cookie name fallback cookie: undefined, }, mockCookies, @@ -284,6 +319,9 @@ describe('AstroSession - Configuration', () => { }); }); +// #endregion + +// #region Sparse Data Operations describe('AstroSession - Sparse Data Operations', () => { it('should handle multiple operations in sparse mode', async () => { const existingData = stringify( @@ -297,7 +335,7 @@ describe('AstroSession - Sparse Data Operations', () => { const mockStorage = { get: async () => existingData, setItem: async () => {}, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); @@ -318,7 +356,7 @@ describe('AstroSession - Sparse Data Operations', () => { const mockStorage = { get: async () => existingData, setItem: async () => {}, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); @@ -334,13 +372,13 @@ describe('AstroSession - Sparse Data Operations', () => { }); it('should maintain deletion after persistence', async () => { - let storedData; + let storedData: string | undefined; const mockStorage = { - get: async () => storedData || stringify(new Map([['key', 'value']])), - setItem: async (_key, value) => { + get: async () => storedData ?? stringify(new Map([['key', 'value']])), + setItem: async (_key: string, value: string) => { storedData = value; }, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); @@ -351,7 +389,7 @@ describe('AstroSession - Sparse Data Operations', () => { const newSession = createSession(defaultConfig, defaultMockCookies, { get: async () => storedData, setItem: async () => {}, - }); + } as unknown as Storage); assert.equal(await newSession.get('key'), undefined); }); @@ -361,7 +399,7 @@ describe('AstroSession - Sparse Data Operations', () => { const mockStorage = { get: async () => existingData, setItem: async () => {}, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); @@ -374,16 +412,19 @@ describe('AstroSession - Sparse Data Operations', () => { }); }); +// #endregion + +// #region Cleanup Operations describe('AstroSession - Cleanup Operations', () => { it('should clean up destroyed sessions on persist', async () => { - const removedKeys = new Set(); + const removedKeys = new Set(); const mockStorage = { get: async () => stringify(new Map([['key', 'value']])), setItem: async () => {}, - removeItem: async (key) => { + removeItem: async (key: string) => { removedKeys.add(key); }, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); @@ -397,18 +438,18 @@ describe('AstroSession - Cleanup Operations', () => { // Simulate end of request await session[PERSIST_SYMBOL](); - assert.ok(removedKeys.has(oldId), `Session ${oldId} should be removed`); + assert.ok(removedKeys.has(String(oldId)), `Session ${oldId} should be removed`); }); it("should destroy sessions that haven't been loaded", async () => { - const removedKeys = new Set(); + const removedKeys = new Set(); const mockStorage = { get: async () => stringify(new Map([['key', 'value']])), setItem: async () => {}, - removeItem: async (key) => { + removeItem: async (key: string) => { removedKeys.add(key); }, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); session.destroy(); @@ -419,12 +460,15 @@ describe('AstroSession - Cleanup Operations', () => { }); }); +// #endregion + +// #region Cookie Security describe('AstroSession - Cookie Security', () => { it('should enforce httpOnly cookie setting', async () => { - let cookieOptions; - const mockCookies = { + let cookieOptions: AstroCookieSetOptions | undefined; + const mockCookies: MockCookies = { ...defaultMockCookies, - set: (_name, _value, options) => { + set: (_name: string, _value: string, options?: AstroCookieSetOptions) => { cookieOptions = options; }, }; @@ -432,22 +476,22 @@ describe('AstroSession - Cookie Security', () => { const session = createSession( { ...defaultConfig, - cookieOptions: { - httpOnly: false, - }, + // @ts-expect-error — intentionally testing that AstroSession ignores httpOnly: false + // and always enforces httpOnly: true regardless of user config + cookie: { httpOnly: false }, }, mockCookies, ); session.set('key', 'value'); - assert.equal(cookieOptions.httpOnly, true); + assert.equal(cookieOptions?.httpOnly, true); }); it('should set secure and sameSite by default in production', async () => { - let cookieOptions; - const mockCookies = { + let cookieOptions: AstroCookieSetOptions | undefined; + const mockCookies: MockCookies = { ...defaultMockCookies, - set: (_name, _value, options) => { + set: (_name: string, _value: string, options?: AstroCookieSetOptions) => { cookieOptions = options; }, }; @@ -455,27 +499,30 @@ describe('AstroSession - Cookie Security', () => { const session = createSession(defaultConfig, mockCookies); session.set('key', 'value'); - assert.equal(cookieOptions.secure, true); - assert.equal(cookieOptions.sameSite, 'lax'); + assert.equal(cookieOptions?.secure, true); + assert.equal(cookieOptions?.sameSite, 'lax'); }); it('should set secure to false in development', async () => { - let cookieOptions; - const mockCookies = { + let cookieOptions: AstroCookieSetOptions | undefined; + const mockCookies: MockCookies = { ...defaultMockCookies, - set: (_name, _value, options) => { + set: (_name: string, _value: string, options?: AstroCookieSetOptions) => { cookieOptions = options; }, }; - const session = createSession(defaultConfig, mockCookies, undefined, 'development'); + const session = createSession(defaultConfig, mockCookies, null, 'development'); session.set('key', 'value'); - assert.equal(cookieOptions.secure, false); - assert.equal(cookieOptions.sameSite, 'lax'); + assert.equal(cookieOptions?.secure, false); + assert.equal(cookieOptions?.sameSite, 'lax'); }); }); +// #endregion + +// #region Storage Errors describe('AstroSession - Storage Errors', () => { it('should handle storage setItem failures', async () => { const mockStorage = { @@ -483,7 +530,7 @@ describe('AstroSession - Storage Errors', () => { setItem: async () => { throw new Error('Storage full'); }, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); session.set('key', 'value'); @@ -495,7 +542,7 @@ describe('AstroSession - Storage Errors', () => { const mockStorage = { get: async () => stringify({ notAMap: true }), setItem: async () => {}, - }; + } as unknown as Storage; const session = createSession(defaultConfig, defaultMockCookies, mockStorage); @@ -505,3 +552,5 @@ describe('AstroSession - Storage Errors', () => { ); }); }); + +// #endregion Storage Errors diff --git a/packages/astro/test/units/vite-plugin-astro-server/controller.test.js b/packages/astro/test/units/vite-plugin-astro-server/controller.test.ts similarity index 61% rename from packages/astro/test/units/vite-plugin-astro-server/controller.test.js rename to packages/astro/test/units/vite-plugin-astro-server/controller.test.ts index 37fa4dfc42ce..d63e5c62b4bc 100644 --- a/packages/astro/test/units/vite-plugin-astro-server/controller.test.js +++ b/packages/astro/test/units/vite-plugin-astro-server/controller.test.ts @@ -1,5 +1,6 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { EnvironmentModuleNode } from 'vite'; import { createLoader } from '../../../dist/core/module-loader/index.js'; import { createController, @@ -9,8 +10,8 @@ import { describe('vite-plugin-astro-server', () => { describe('controller', () => { it('calls the onError method when an error occurs in the handler', async () => { - const controller = createController({ loader: createLoader() }); - let error = undefined; + const controller = createController({ loader: createLoader({}) }); + let error: unknown = undefined; await runWithErrorHandling({ controller, pathname: '/', @@ -19,6 +20,7 @@ describe('vite-plugin-astro-server', () => { }, onError(err) { error = err; + return err instanceof Error ? err : undefined; }, }); assert.equal(typeof error !== 'undefined', true); @@ -26,14 +28,16 @@ describe('vite-plugin-astro-server', () => { }); it('sets the state to error when an error occurs in the handler', async () => { - const controller = createController({ loader: createLoader() }); + const controller = createController({ loader: createLoader({}) }); await runWithErrorHandling({ controller, pathname: '/', run() { throw new Error('oh no'); }, - onError() {}, + onError() { + return undefined; + }, }); assert.equal(controller.state.state, 'error'); }); @@ -47,7 +51,7 @@ describe('vite-plugin-astro-server', () => { }, }); const controller = createController({ loader }); - loader.events.emit('file-change'); + loader.events.emit('file-change', ['/some/file.ts']); assert.equal(reloads, 0); await runWithErrorHandling({ controller, @@ -55,10 +59,12 @@ describe('vite-plugin-astro-server', () => { run() { throw new Error('oh no'); }, - onError() {}, + onError() { + return undefined; + }, }); assert.equal(reloads, 0); - loader.events.emit('file-change'); + loader.events.emit('file-change', ['/some/file.ts']); assert.equal(reloads, 1); }); @@ -71,7 +77,7 @@ describe('vite-plugin-astro-server', () => { }, }); const controller = createController({ loader }); - loader.events.emit('file-change'); + loader.events.emit('file-change', ['/some/file.ts']); assert.equal(reloads, 0); await runWithErrorHandling({ controller, @@ -79,34 +85,41 @@ describe('vite-plugin-astro-server', () => { run() { throw new Error('oh no'); }, - onError() {}, + onError() { + return undefined; + }, }); assert.equal(reloads, 0); - loader.events.emit('file-change'); + loader.events.emit('file-change', ['/some/file.ts']); assert.equal(reloads, 1); - loader.events.emit('file-change'); + loader.events.emit('file-change', ['/some/file.ts']); assert.equal(reloads, 2); await runWithErrorHandling({ controller, pathname: '/', // No error here - run() {}, + async run() {}, + onError() { + return undefined; + }, }); - loader.events.emit('file-change'); + loader.events.emit('file-change', ['/some/file.ts']); assert.equal(reloads, 2); }); it('Invalidates broken modules when a change occurs in an error state', async () => { - const mods = [ - { id: 'one', ssrError: new Error('one') }, - { id: 'two', ssrError: null }, - { id: 'three', ssrError: new Error('three') }, + const mods: EnvironmentModuleNode[] = [ + { id: 'one', ssrError: new Error('one') } as unknown as EnvironmentModuleNode, + { id: 'two', ssrError: null } as unknown as EnvironmentModuleNode, + { id: 'three', ssrError: new Error('three') } as unknown as EnvironmentModuleNode, ]; const loader = createLoader({ eachModule(cb) { - return mods.forEach(cb); + return mods.forEach((mod, index, arr) => + cb(mod, String(index), arr as unknown as Map), + ); }, invalidateModule(mod) { mod.ssrError = null; @@ -120,16 +133,21 @@ describe('vite-plugin-astro-server', () => { run() { throw new Error('oh no'); }, - onError() {}, + onError() { + return undefined; + }, }); - loader.events.emit('file-change'); + loader.events.emit('file-change', ['/some/file.ts']); - assert.deepEqual(mods, [ - { id: 'one', ssrError: null }, - { id: 'two', ssrError: null }, - { id: 'three', ssrError: null }, - ]); + assert.deepEqual( + mods.map((m) => ({ id: m.id, ssrError: m.ssrError })), + [ + { id: 'one', ssrError: null }, + { id: 'two', ssrError: null }, + { id: 'three', ssrError: null }, + ], + ); }); }); }); diff --git a/packages/astro/test/units/vite-plugin-astro/compile.test.js b/packages/astro/test/units/vite-plugin-astro/compile.test.ts similarity index 62% rename from packages/astro/test/units/vite-plugin-astro/compile.test.js rename to packages/astro/test/units/vite-plugin-astro/compile.test.ts index 6f0daed3b5ef..a06614bbac45 100644 --- a/packages/astro/test/units/vite-plugin-astro/compile.test.js +++ b/packages/astro/test/units/vite-plugin-astro/compile.test.ts @@ -3,25 +3,49 @@ import { describe, it } from 'node:test'; import { pathToFileURL } from 'node:url'; import { init, parse } from 'es-module-lexer'; import { resolveConfig } from 'vite'; +import type { InlineConfig } from 'vite'; import { compileAstro } from '../../../dist/vite-plugin-astro/compile.js'; +import type { AstroConfig } from '../../../dist/types/public/config.js'; +import type { CompileProps } from '../../../dist/core/compile/compile.js'; -/** - * @param {string} source - * @param {string} id - */ -async function compile(source, id, inlineConfig = {}) { +// #region Helpers + +/** Minimal AstroConfig stub for compile tests. */ +function makeAstroConfig(overrides: Partial = {}): AstroConfig { + return { + root: pathToFileURL('/'), + base: '/', + experimental: {}, + ...overrides, + } as AstroConfig; +} + +async function compile(source: string, id: string, inlineConfig: InlineConfig = {}) { const viteConfig = await resolveConfig({ configFile: false, ...inlineConfig }, 'serve'); - return await compileAstro({ - compileProps: { - astroConfig: { root: pathToFileURL('/'), base: '/', experimental: {} }, - viteConfig, - filename: id, - source, - }, + // compileAstro's CompileAstroOption traces back to src/AstroConfig via rewriteRelativeImportExtensions, + // but we import from dist/. The types are structurally identical at runtime; cast to bridge the gap. + const props: CompileProps = { + astroConfig: makeAstroConfig(), + viteConfig, + toolbarEnabled: false, + filename: id, + source, + }; + return ( + compileAstro as (opts: { + compileProps: CompileProps; + astroFileToCompileMetadata: Map; + }) => ReturnType + )({ + compileProps: props, astroFileToCompileMetadata: new Map(), }); } +// #endregion + +// #region Tests + describe('astro full compile', () => { it('should compile a single file', async () => { const result = await compile(`

Hello World

`, '/src/components/index.astro'); @@ -53,8 +77,8 @@ const name = 'world

Hello {name}

`, '/src/components/index.astro', ); - } catch (e) { - assert.equal(e.message.includes('Unterminated string literal'), true); + } catch (e: unknown) { + assert.equal((e as Error).message.includes('Unterminated string literal'), true); } assert.equal(result, undefined); }); @@ -70,7 +94,7 @@ const name = 'world }); describe('when the code contains syntax that is transformed by esbuild', () => { - let code = `\ + const code = `\ --- using x = {} ---`; @@ -88,3 +112,5 @@ using x = {} }); }); }); + +// #endregion diff --git a/packages/astro/test/units/vite-plugin-astro/hmr.test.js b/packages/astro/test/units/vite-plugin-astro/hmr.test.ts similarity index 100% rename from packages/astro/test/units/vite-plugin-astro/hmr.test.js rename to packages/astro/test/units/vite-plugin-astro/hmr.test.ts diff --git a/packages/astro/test/units/vite-plugin-html/escape.test.js b/packages/astro/test/units/vite-plugin-html/escape.test.ts similarity index 93% rename from packages/astro/test/units/vite-plugin-html/escape.test.js rename to packages/astro/test/units/vite-plugin-html/escape.test.ts index a46ea5d7a3c2..123e8a5774d3 100644 --- a/packages/astro/test/units/vite-plugin-html/escape.test.js +++ b/packages/astro/test/units/vite-plugin-html/escape.test.ts @@ -76,7 +76,7 @@ describe('vite-plugin-html: escape utilities', () => { }); describe('vite-plugin-html: escape transformer', () => { - async function testEscapeTransform(html) { + async function testEscapeTransform(html: string) { const s = new MagicString(html); const processor = rehype().data('settings', { fragment: true }).use(rehypeEscape, { s }); @@ -85,7 +85,7 @@ describe('vite-plugin-html: escape transformer', () => { } it('escapes text content', async () => { - const result = await testEscapeTransform('
${foo}
', '
\\${foo}
'); + const result = await testEscapeTransform('
${foo}
'); assert.equal(result, '
\\${foo}
'); }); @@ -96,14 +96,13 @@ describe('vite-plugin-html: escape transformer', () => { }); it('escapes attribute names with template literal characters', async () => { - const result = await testEscapeTransform('', ''); + const result = await testEscapeTransform(''); assert.equal(result, ''); }); it('escapes attribute values with template literal characters', async () => { const result = await testEscapeTransform( '', - '', ); assert.equal(result, ''); }); @@ -118,7 +117,7 @@ describe('vite-plugin-html: escape transformer', () => { it('escapes complex nested structures', async () => { const input = ''; const expected = ''; - const result = await testEscapeTransform(input, expected); + const result = await testEscapeTransform(input); assert.equal(result, expected); }); @@ -132,14 +131,14 @@ describe('vite-plugin-html: escape transformer', () => { it('preserves content without template literal characters', async () => { const input = '
Hello world!
'; - const result = await testEscapeTransform(input, input); + const result = await testEscapeTransform(input); assert.equal(result, input); }); it('handles empty attributes correctly', async () => { const input = '
'; const expected = '
'; - const result = await testEscapeTransform(input, expected); + const result = await testEscapeTransform(input); assert.equal(result, expected); }); }); diff --git a/packages/astro/test/units/vite-plugin-html/slots.test.js b/packages/astro/test/units/vite-plugin-html/slots.test.ts similarity index 98% rename from packages/astro/test/units/vite-plugin-html/slots.test.js rename to packages/astro/test/units/vite-plugin-html/slots.test.ts index 0b6694992ae6..829fd5511b7a 100644 --- a/packages/astro/test/units/vite-plugin-html/slots.test.js +++ b/packages/astro/test/units/vite-plugin-html/slots.test.ts @@ -6,7 +6,7 @@ import { VFile } from 'vfile'; import rehypeSlots, { SLOT_PREFIX } from '../../../dist/vite-plugin-html/transform/slots.js'; describe('vite-plugin-html: slot transformer', () => { - async function testSlotTransform(html) { + async function testSlotTransform(html: string) { const s = new MagicString(html); const processor = rehype().data('settings', { fragment: true }).use(rehypeSlots, { s }); diff --git a/packages/astro/test/units/vite-plugin-html/transform.test.js b/packages/astro/test/units/vite-plugin-html/transform.test.ts similarity index 100% rename from packages/astro/test/units/vite-plugin-html/transform.test.js rename to packages/astro/test/units/vite-plugin-html/transform.test.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c8e72fb6579..9df2f6fee239 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4501,15 +4501,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/units/_temp-fixtures: - dependencies: - '@astrojs/mdx': - specifier: workspace:* - version: link:../../../../integrations/mdx - astro: - specifier: workspace:* - version: link:../../.. - packages/create-astro: dependencies: '@astrojs/cli-kit': From 118a868dc47f412e6473e9a685e16ebb7ae98589 Mon Sep 17 00:00:00 2001 From: ematipico Date: Tue, 31 Mar 2026 13:48:56 +0100 Subject: [PATCH 2/3] use inferrable types --- biome.jsonc | 3 ++- eslint.config.js | 1 + packages/astro/test/units/server-islands/endpoint.test.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index c661d7f0e49e..ccb4d8651989 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -46,7 +46,8 @@ // Enforce separate type imports for type-only imports to avoid bundling unneeded code "useImportType": "error", "useExportType": "error", - "useNumberNamespace": "warn" + "useNumberNamespace": "warn", + "noInferrableTypes": "error" }, "suspicious": { // This one is specific to catch `console.log`. The rest of logs are permitted diff --git a/eslint.config.js b/eslint.config.js index 55aa79531465..8686de7d256f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -65,6 +65,7 @@ export default [ '@typescript-eslint/consistent-indexed-object-style': 'off', '@typescript-eslint/consistent-type-definitions': 'off', '@typescript-eslint/dot-notation': 'off', + '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-base-to-string': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-floating-promises': 'off', diff --git a/packages/astro/test/units/server-islands/endpoint.test.ts b/packages/astro/test/units/server-islands/endpoint.test.ts index 5592635949d1..b80f17c8988c 100644 --- a/packages/astro/test/units/server-islands/endpoint.test.ts +++ b/packages/astro/test/units/server-islands/endpoint.test.ts @@ -40,7 +40,7 @@ function makeInvalidPostRequest(body: Record) { }); } -function makeMethodRequest(method: string = 'GET') { +function makeMethodRequest(method = 'GET') { return new Request('http://localhost/_server-islands/Island', { method }); } From 9ff7c91f698123045c7f1ca2236577d9c9f8f804 Mon Sep 17 00:00:00 2001 From: ematipico Date: Tue, 31 Mar 2026 19:33:32 +0100 Subject: [PATCH 3/3] fix types --- .../test/units/content-collections/mutable-data-store.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/astro/test/units/content-collections/mutable-data-store.test.ts b/packages/astro/test/units/content-collections/mutable-data-store.test.ts index ce27408b838b..a15b2590aa3d 100644 --- a/packages/astro/test/units/content-collections/mutable-data-store.test.ts +++ b/packages/astro/test/units/content-collections/mutable-data-store.test.ts @@ -58,6 +58,7 @@ describe('MutableDataStore', () => { const validId = imageSrcToImportId('./images/seed.webp', entryFilePath); const staleId = imageSrcToImportId('./images/non-existing.jpg', entryFilePath); + assert.ok(!!validId); assert.ok( content.includes(validId), `content-assets.mjs should reference the valid image import "${validId}"`,