From 5b2ecea52bf01a9af1b8248f3ece243c75ac9594 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 22 Feb 2026 23:51:47 +0100 Subject: [PATCH] test: add custom toMatchObject matcher for @std/expect compatibility Implement custom Vitest matcher that replicates @std/expect's toMatchObject semantics for arrays. The matcher handles sparse array subset matching where only defined indices are compared, matching the behavior of @std/expect's spread-based algorithm in _equal.ts. This fixes 8 failing tests in queue test files that use toMatchObject with sparse arrays created via new Array(capacity). --- tests/vitest.setup.mjs | 149 +++++++++++++++++++++++++++++++++++++++++ vitest.config.ts | 1 + 2 files changed, 150 insertions(+) create mode 100644 tests/vitest.setup.mjs diff --git a/tests/vitest.setup.mjs b/tests/vitest.setup.mjs new file mode 100644 index 000000000..29d8468b3 --- /dev/null +++ b/tests/vitest.setup.mjs @@ -0,0 +1,149 @@ +import { expect } from 'vitest' + +/** + * Format a value for display in error messages + * @param value + * @param indent + */ +function formatValue (value, indent = 0) { + const spaces = ' '.repeat(indent) + if (value === null) return 'null' + if (value === undefined) return 'undefined' + if (typeof value === 'string') return JSON.stringify(value) + if (typeof value === 'number' || typeof value === 'boolean') { return String(value) } + if (Array.isArray(value)) { + if (value.length === 0) return '[]' + const items = value + .map((item, index) => { + if (!(index in value)) return `${spaces} ` + return `${spaces} ${formatValue(item, indent + 1)}` + }) + .join(',\n') + return `[\n${items}\n${spaces}]` + } + if (typeof value === 'object') { + if ( + typeof value.asymmetricMatch === 'function' && + value.toString !== Object.prototype.toString + ) { + return value.toString() + } + const keys = Object.keys(value) + if (keys.length === 0) return '{}' + const items = keys + .map(key => `${spaces} ${key}: ${formatValue(value[key], indent + 1)}`) + .join(',\n') + return `{\n${items}\n${spaces}}` + } + return String(value) +} + +/** + * Custom matcher that replicates @std/expect's toMatchObject semantics for arrays. + * + * Key difference from Vitest's built-in toMatchObject: + * - For arrays: only checks that each element in `expected` matches the corresponding + * element in `received` at the same index. Extra elements in `received` are ignored. + * - For objects: checks that all keys in `expected` exist in `received` with matching values. + * Extra keys in `received` are ignored. + * + * This matches the behavior of @std/expect from Deno which uses subsetEquality. + * @param received + * @param expected + * @param asymmetricMatchers + */ +function subsetMatch (received, expected, asymmetricMatchers) { + // Handle asymmetric matchers (like expect.any(Number)) + if ( + expected != null && + typeof expected === 'object' && + typeof expected.asymmetricMatch === 'function' + ) { + return expected.asymmetricMatch(received) + } + + // Handle null/undefined + if (expected === null || expected === undefined) { + return received === expected + } + + // Handle primitives + if (typeof expected !== 'object') { + return Object.is(received, expected) + } + + // Handle Date objects + if (expected instanceof Date) { + return received instanceof Date && received.getTime() === expected.getTime() + } + + // Handle RegExp objects + if (expected instanceof RegExp) { + return ( + received instanceof RegExp && + received.source === expected.source && + received.flags === expected.flags + ) + } + + // Handle arrays - this is the key difference from Vitest's toMatchObject + // We only check elements up to expected.length + if (Array.isArray(expected)) { + if (!Array.isArray(received)) { + return false + } + // Check each element in expected matches the corresponding element in received + for (let i = 0; i < expected.length; i++) { + // Skip sparse array slots (undefined created by `new Array(n)`) + if (!(i in expected)) { + continue + } + if (!(i in received)) { + return false + } + if (!subsetMatch(received[i], expected[i], asymmetricMatchers)) { + return false + } + } + return true + } + + // Handle plain objects - subset matching + if (typeof received !== 'object' || received === null) { + return false + } + + // Check all keys in expected exist in received with matching values + for (const key of Object.keys(expected)) { + if (!(key in received)) { + return false + } + if (!subsetMatch(received[key], expected[key], asymmetricMatchers)) { + return false + } + } + + return true +} + +expect.extend({ + /** + * Custom toMatchObject that replicates @std/expect semantics. + * This overrides Vitest's built-in toMatchObject for array subset matching. + * @param received + * @param expected + */ + toMatchObject (received, expected) { + const pass = subsetMatch(received, expected) + + return { + message: () => { + if (pass) { + return `expected ${formatValue(received)} not to match object ${formatValue(expected)}` + } + return `expected ${formatValue(received)} to match object ${formatValue(expected)}` + }, + pass, + } + }, +}) diff --git a/vitest.config.ts b/vitest.config.ts index 21c98edaa..e80f9e30d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -18,6 +18,7 @@ export default defineConfig({ sequence: { hooks: 'list', }, + setupFiles: ['./tests/vitest.setup.mjs'], testTimeout: 30000, }, }) -- 2.43.0