@coherent.js/tooling 1.1.0 → 2.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/testing/matchers.js"],
4
- "sourcesContent": ["/**\n * Coherent.js Custom Test Matchers\n * \n * Custom matchers for testing Coherent.js components\n * Compatible with Vitest, Jest, and other testing frameworks\n * \n * @module testing/matchers\n */\n\n/**\n * Custom matchers for Coherent.js testing\n */\nexport const customMatchers = {\n /**\n * Check if element has specific text\n */\n toHaveText(received, expected) {\n const pass = received && received.text === expected;\n \n return {\n pass,\n message: () => pass\n ? `Expected element not to have text \"${expected}\"`\n : `Expected element to have text \"${expected}\", but got \"${received?.text || 'null'}\"`\n };\n },\n\n /**\n * Check if element contains text\n */\n toContainText(received, expected) {\n const pass = received && received.text && received.text.includes(expected);\n \n return {\n pass,\n message: () => pass\n ? `Expected element not to contain text \"${expected}\"`\n : `Expected element to contain text \"${expected}\", but got \"${received?.text || 'null'}\"`\n };\n },\n\n /**\n * Check if element has specific class\n */\n toHaveClass(received, expected) {\n const pass = received && received.className && received.className.includes(expected);\n \n return {\n pass,\n message: () => pass\n ? `Expected element not to have class \"${expected}\"`\n : `Expected element to have class \"${expected}\", but got \"${received?.className || 'null'}\"`\n };\n },\n\n /**\n * Check if element exists\n */\n toBeInTheDocument(received) {\n const pass = received && received.exists === true;\n \n return {\n pass,\n message: () => pass\n ? 'Expected element not to be in the document'\n : 'Expected element to be in the document'\n };\n },\n\n /**\n * Check if element is visible (has content)\n */\n toBeVisible(received) {\n const pass = received && received.text && received.text.trim().length > 0;\n \n return {\n pass,\n message: () => pass\n ? 'Expected element not to be visible'\n : 'Expected element to be visible (have text content)'\n };\n },\n\n /**\n * Check if element is empty\n */\n toBeEmpty(received) {\n const pass = !received || !received.text || received.text.trim().length === 0;\n \n return {\n pass,\n message: () => pass\n ? 'Expected element not to be empty'\n : 'Expected element to be empty'\n };\n },\n\n /**\n * Check if HTML contains specific string\n */\n toContainHTML(received, expected) {\n const html = received?.html || received;\n const pass = typeof html === 'string' && html.includes(expected);\n \n return {\n pass,\n message: () => pass\n ? `Expected HTML not to contain \"${expected}\"`\n : `Expected HTML to contain \"${expected}\"`\n };\n },\n\n /**\n * Check if element has attribute\n */\n toHaveAttribute(received, attribute, value) {\n const html = received?.html || '';\n const regex = new RegExp(`${attribute}=\"([^\"]*)\"`, 'i');\n const match = html.match(regex);\n \n const pass = value !== undefined\n ? match && match[1] === value\n : match !== null;\n \n return {\n pass,\n message: () => {\n if (value !== undefined) {\n return pass\n ? `Expected element not to have attribute ${attribute}=\"${value}\"`\n : `Expected element to have attribute ${attribute}=\"${value}\", but got \"${match?.[1] || 'none'}\"`;\n }\n return pass\n ? `Expected element not to have attribute ${attribute}`\n : `Expected element to have attribute ${attribute}`;\n }\n };\n },\n\n /**\n * Check if component matches snapshot\n */\n toMatchSnapshot(received) {\n const _snapshot = received?.toSnapshot ? received.toSnapshot() : received;\n \n // This would integrate with the testing framework's snapshot system\n return {\n pass: true,\n message: () => 'Snapshot comparison'\n };\n },\n\n /**\n * Check if element has specific tag name\n */\n toHaveTagName(received, tagName) {\n const html = received?.html || '';\n const regex = new RegExp(`<${tagName}[^>]*>`, 'i');\n const pass = regex.test(html);\n \n return {\n pass,\n message: () => pass\n ? `Expected element not to have tag name \"${tagName}\"`\n : `Expected element to have tag name \"${tagName}\"`\n };\n },\n\n /**\n * Check if render result contains element\n */\n toContainElement(received, element) {\n const html = received?.html || received;\n const elementHtml = element?.html || element;\n const pass = typeof html === 'string' && html.includes(elementHtml);\n \n return {\n pass,\n message: () => pass\n ? 'Expected not to contain element'\n : 'Expected to contain element'\n };\n },\n\n /**\n * Check if mock was called\n */\n toHaveBeenCalled(received) {\n const pass = received?.mock?.calls?.length > 0;\n \n return {\n pass,\n message: () => pass\n ? 'Expected mock not to have been called'\n : 'Expected mock to have been called'\n };\n },\n\n /**\n * Check if mock was called with specific args\n */\n toHaveBeenCalledWith(received, ...expectedArgs) {\n const calls = received?.mock?.calls || [];\n const pass = calls.some(call => \n call.length === expectedArgs.length &&\n call.every((arg, i) => arg === expectedArgs[i])\n );\n \n return {\n pass,\n message: () => pass\n ? `Expected mock not to have been called with ${JSON.stringify(expectedArgs)}`\n : `Expected mock to have been called with ${JSON.stringify(expectedArgs)}`\n };\n },\n\n /**\n * Check if mock was called N times\n */\n toHaveBeenCalledTimes(received, times) {\n const callCount = received?.mock?.calls?.length || 0;\n const pass = callCount === times;\n \n return {\n pass,\n message: () => pass\n ? `Expected mock not to have been called ${times} times`\n : `Expected mock to have been called ${times} times, but was called ${callCount} times`\n };\n },\n\n /**\n * Check if component rendered successfully\n */\n toRenderSuccessfully(received) {\n const pass = received && received.html && received.html.length > 0;\n \n return {\n pass,\n message: () => pass\n ? 'Expected component not to render successfully'\n : 'Expected component to render successfully'\n };\n },\n\n /**\n * Check if HTML is valid\n */\n toBeValidHTML(received) {\n const html = received?.html || received;\n \n // Basic HTML validation\n const openTags = (html.match(/<[^/][^>]*>/g) || []).length;\n const closeTags = (html.match(/<\\/[^>]+>/g) || []).length;\n const selfClosing = (html.match(/<[^>]+\\/>/g) || []).length;\n \n const pass = openTags === closeTags + selfClosing;\n \n return {\n pass,\n message: () => pass\n ? 'Expected HTML not to be valid'\n : `Expected HTML to be valid (open: ${openTags}, close: ${closeTags}, self-closing: ${selfClosing})`\n };\n }\n};\n\n/**\n * Extend expect with custom matchers\n * \n * @param {Object} expect - Expect function from testing framework\n * \n * @example\n * import { expect } from 'vitest';\n * import { extendExpect } from '@coherent.js/tooling/testing/matchers';\n * \n * extendExpect(expect);\n * \n * // Now you can use custom matchers\n * expect(element).toHaveText('Hello');\n */\nexport function extendExpect(expect) {\n if (expect && expect.extend) {\n expect.extend(customMatchers);\n } else {\n console.warn('Could not extend expect - expect.extend not available');\n }\n}\n\n/**\n * Create assertion helpers\n */\nexport const assertions = {\n /**\n * Assert element has text\n */\n assertHasText(element, text) {\n if (!element || element.text !== text) {\n throw new Error(`Expected element to have text \"${text}\", but got \"${element?.text || 'null'}\"`);\n }\n },\n\n /**\n * Assert element exists\n */\n assertExists(element) {\n if (!element || !element.exists) {\n throw new Error('Expected element to exist');\n }\n },\n\n /**\n * Assert element has class\n */\n assertHasClass(element, className) {\n if (!element || !element.className || !element.className.includes(className)) {\n throw new Error(`Expected element to have class \"${className}\"`);\n }\n },\n\n /**\n * Assert HTML contains string\n */\n assertContainsHTML(html, substring) {\n const htmlString = html?.html || html;\n if (!htmlString || !htmlString.includes(substring)) {\n throw new Error(`Expected HTML to contain \"${substring}\"`);\n }\n },\n\n /**\n * Assert component rendered\n */\n assertRendered(result) {\n if (!result || !result.html || result.html.length === 0) {\n throw new Error('Expected component to render');\n }\n }\n};\n\n/**\n * Export all matchers and utilities\n */\nexport default {\n customMatchers,\n extendExpect,\n assertions\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Coherent.js Custom Test Matchers\n *\n * Custom matchers for testing Coherent.js components\n * Compatible with Vitest, Jest, and other testing frameworks\n *\n * The matchers accept what this package's own helpers return: a render\n * result from `renderComponent()` (`{ html }`), a match from its query\n * helpers (`{ html, text, exists }`), or a plain HTML string. Element\n * matchers (`toHaveClass`, `toHaveAttribute`, `toHaveTagName`) look at the\n * first element in that HTML.\n *\n * None of them reuses the name of a matcher Vitest or Jest already ships:\n * `expect.extend()` would replace the built-in for the whole test run.\n * Snapshot with `expect(result.toSnapshot()).toMatchSnapshot()`, and assert\n * on mocks (`vi.fn()` or this package's `createMock()`) with the built-in\n * `toHaveBeenCalled*` matchers.\n *\n * @module testing/matchers\n */\n\n/** Elements that never have a closing tag. */\nconst VOID_ELEMENTS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',\n 'link', 'meta', 'param', 'source', 'track', 'wbr'\n]);\n\n/** Elements whose content is raw text, not markup. */\nconst RAW_TEXT_ELEMENTS = new Set(['script', 'style', 'textarea', 'title']);\n\nconst NAMED_ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\", nbsp: '\u00A0' };\n\nfunction decodeEntities(text) {\n return text.replace(/&(#x[0-9a-f]+|#\\d+|[a-z]+);/gi, (entity, body) => {\n if (body[0] === '#') {\n const codePoint = body[1] === 'x' || body[1] === 'X'\n ? Number.parseInt(body.slice(2), 16)\n : Number.parseInt(body.slice(1), 10);\n return codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : entity;\n }\n return NAMED_ENTITIES[body.toLowerCase()] ?? entity;\n });\n}\n\n/** The HTML string behind a render result, a query match, or a string. */\nfunction htmlOf(received) {\n if (typeof received === 'string') return received;\n if (received && typeof received.html === 'string') return received.html;\n return null;\n}\n\n/**\n * Text content: a query match's `text`, or the HTML with its tags removed.\n * Entities are decoded, so assertions use the text a reader sees.\n */\nfunction textOf(received) {\n if (received && typeof received === 'object' && typeof received.text === 'string') {\n return decodeEntities(received.text);\n }\n const html = htmlOf(received);\n if (html === null) return null;\n return decodeEntities(stripTags(html));\n}\n\n/**\n * Remove tags (`<...>`) in one linear pass; a `<` with no `>` after it stays.\n * /<[^>]*>/g rescans the rest of the input from every `<` when no `>`\n * follows: seconds on '<<<<\u2026' of 50 KB.\n */\nfunction stripTags(html) {\n let out = '';\n let cursor = 0;\n while (cursor < html.length) {\n const open = html.indexOf('<', cursor);\n if (open === -1) break;\n const close = html.indexOf('>', open + 1);\n if (close === -1) break;\n out += html.slice(cursor, open);\n cursor = close + 1;\n }\n return out + html.slice(cursor);\n}\n\nconst isSpace = (ch) => ch === ' ' || ch === '\\n' || ch === '\\t' || ch === '\\r' || ch === '\\f';\n\n/**\n * Parse the first opening tag in `html` into its name and attributes.\n * Hand-rolled (no backtracking regex), so hostile input stays linear.\n *\n * @returns {{ tagName: string, attributes: Map<string, string> } | null}\n */\nfunction parseOpeningTag(html) {\n const start = html.search(/<[a-zA-Z]/);\n if (start === -1) return null;\n\n let i = start + 1;\n let tagName = '';\n while (i < html.length && /[\\w:-]/.test(html[i])) tagName += html[i++];\n\n const attributes = new Map();\n while (i < html.length) {\n while (i < html.length && isSpace(html[i])) i++;\n if (i >= html.length || html[i] === '>') break;\n if (html[i] === '/') {\n i++;\n continue;\n }\n\n let name = '';\n while (i < html.length && !isSpace(html[i]) && html[i] !== '=' && html[i] !== '>' && html[i] !== '/') {\n name += html[i++];\n }\n while (i < html.length && isSpace(html[i])) i++;\n\n let value = '';\n if (html[i] === '=') {\n i++;\n while (i < html.length && isSpace(html[i])) i++;\n const quote = html[i];\n if (quote === '\"' || quote === \"'\") {\n const end = html.indexOf(quote, i + 1);\n if (end === -1) break;\n value = html.slice(i + 1, end);\n i = end + 1;\n } else {\n while (i < html.length && !isSpace(html[i]) && html[i] !== '>') value += html[i++];\n }\n }\n if (name) attributes.set(name.toLowerCase(), decodeEntities(value));\n }\n\n return { tagName: tagName.toLowerCase(), attributes };\n}\n\n/** Class tokens of the first element (or a match's `className`), or null. */\nfunction classesOf(received) {\n const html = htmlOf(received);\n const tag = html === null ? null : parseOpeningTag(html);\n let value = tag?.attributes.get('class');\n if (value === undefined && typeof received?.className === 'string') value = received.className;\n return value === undefined ? null : value.split(/\\s+/).filter(Boolean);\n}\n\n/**\n * Check tag balance with a stack, skipping comments, doctypes, void\n * elements and raw-text content. Linear: every '<' is visited once.\n *\n * @returns {string|null} Why the HTML is invalid, or null when it is valid.\n */\nfunction findHTMLError(html) {\n const stack = [];\n let lower = null;\n let i = 0;\n\n while ((i = html.indexOf('<', i)) !== -1) {\n if (html.startsWith('<!--', i)) {\n const end = html.indexOf('-->', i + 4);\n if (end === -1) return 'unterminated comment';\n i = end + 3;\n continue;\n }\n\n const close = html.indexOf('>', i);\n if (close === -1) return 'unterminated tag';\n const tag = html.slice(i + 1, close);\n i = close + 1;\n\n if (tag[0] === '!' || tag[0] === '?') continue; // <!DOCTYPE \u2026>, <?xml \u2026?>\n const match = /^(\\/?)([a-zA-Z][\\w:-]*)/.exec(tag);\n if (!match) continue;\n const name = match[2].toLowerCase();\n\n if (match[1]) {\n if (VOID_ELEMENTS.has(name)) return `</${name}> closes a void element`;\n const open = stack.pop();\n if (open !== name) return open ? `</${name}> does not close <${open}>` : `</${name}> has no opening tag`;\n } else if (!VOID_ELEMENTS.has(name) && !tag.endsWith('/')) {\n stack.push(name);\n if (RAW_TEXT_ELEMENTS.has(name)) {\n lower ??= html.toLowerCase();\n const end = lower.indexOf(`</${name}`, i);\n if (end === -1) return `<${name}> is never closed`;\n i = end;\n }\n }\n }\n\n return stack.length > 0 ? `<${stack[stack.length - 1]}> is never closed` : null;\n}\n\nconst show = (value) => (value === null || value === undefined ? 'nothing' : JSON.stringify(value));\n\n/**\n * Custom matchers for Coherent.js testing\n */\nexport const customMatchers = {\n /**\n * Check if element (or a render result) has exactly this text content\n */\n toHaveText(received, expected) {\n const text = textOf(received);\n const pass = text === expected;\n\n return {\n pass,\n message: () => pass\n ? `Expected element not to have text \"${expected}\"`\n : `Expected element to have text \"${expected}\", but got ${show(text)}`\n };\n },\n\n /**\n * Check if element (or a render result) contains text\n */\n toContainText(received, expected) {\n const text = textOf(received);\n const pass = typeof text === 'string' && text.includes(expected);\n\n return {\n pass,\n message: () => pass\n ? `Expected element not to contain text \"${expected}\"`\n : `Expected element to contain text \"${expected}\", but got ${show(text)}`\n };\n },\n\n /**\n * Check if the element has every given class (whole class tokens:\n * 'btn' does not match 'btn-primary')\n */\n toHaveClass(received, expected) {\n const classes = classesOf(received) ?? [];\n const wanted = String(expected).split(/\\s+/).filter(Boolean);\n const pass = wanted.length > 0 && wanted.every((name) => classes.includes(name));\n\n return {\n pass,\n message: () => pass\n ? `Expected element not to have class \"${expected}\"`\n : `Expected element to have class \"${expected}\", but its classes are ${show(classes.join(' '))}`\n };\n },\n\n /**\n * Check if element exists\n */\n toBeInTheDocument(received) {\n const pass = Boolean(received && received.exists === true);\n\n return {\n pass,\n message: () => pass\n ? 'Expected element not to be in the document'\n : 'Expected element to be in the document'\n };\n },\n\n /**\n * Check if element is visible (has text content)\n */\n toBeVisible(received) {\n const text = textOf(received);\n const pass = typeof text === 'string' && text.trim().length > 0;\n\n return {\n pass,\n message: () => pass\n ? 'Expected element not to be visible'\n : 'Expected element to be visible (have text content)'\n };\n },\n\n /**\n * Check if element is empty (no text content)\n */\n toBeEmpty(received) {\n const text = textOf(received);\n const pass = !text || text.trim().length === 0;\n\n return {\n pass,\n message: () => pass\n ? 'Expected element not to be empty'\n : `Expected element to be empty, but it has text ${show(text)}`\n };\n },\n\n /**\n * Check if HTML contains specific string\n */\n toContainHTML(received, expected) {\n const html = htmlOf(received);\n const pass = typeof html === 'string' && html.includes(expected);\n\n return {\n pass,\n message: () => pass\n ? `Expected HTML not to contain \"${expected}\"`\n : `Expected HTML to contain \"${expected}\"`\n };\n },\n\n /**\n * Check if the element has an attribute (optionally with this value)\n */\n toHaveAttribute(received, attribute, value) {\n const html = htmlOf(received);\n const tag = html === null ? null : parseOpeningTag(html);\n const name = String(attribute).toLowerCase();\n const has = Boolean(tag?.attributes.has(name));\n const actual = has ? tag.attributes.get(name) : undefined;\n const pass = value !== undefined ? has && actual === String(value) : has;\n\n return {\n pass,\n message: () => {\n if (value !== undefined) {\n return pass\n ? `Expected element not to have attribute ${attribute}=\"${value}\"`\n : `Expected element to have attribute ${attribute}=\"${value}\", but got ${has ? show(actual) : 'none'}`;\n }\n return pass\n ? `Expected element not to have attribute ${attribute}`\n : `Expected element to have attribute ${attribute}`;\n }\n };\n },\n\n /**\n * Check if the element has this tag name\n */\n toHaveTagName(received, tagName) {\n const html = htmlOf(received);\n const tag = html === null ? null : parseOpeningTag(html);\n const pass = Boolean(tag) && tag.tagName === String(tagName).toLowerCase();\n\n return {\n pass,\n message: () => pass\n ? `Expected element not to have tag name \"${tagName}\"`\n : `Expected element to have tag name \"${tagName}\", but got ${show(tag?.tagName)}`\n };\n },\n\n /**\n * Check if render result contains element\n */\n toContainElement(received, element) {\n const html = htmlOf(received);\n const elementHtml = htmlOf(element);\n const pass = typeof html === 'string' && typeof elementHtml === 'string' && html.includes(elementHtml);\n\n return {\n pass,\n message: () => pass\n ? 'Expected not to contain element'\n : 'Expected to contain element'\n };\n },\n\n /**\n * Check if component rendered successfully\n */\n toRenderSuccessfully(received) {\n const html = htmlOf(received);\n const pass = typeof html === 'string' && html.length > 0;\n\n return {\n pass,\n message: () => pass\n ? 'Expected component not to render successfully'\n : 'Expected component to render successfully'\n };\n },\n\n /**\n * Check that every tag is closed in order. Void elements (<input>, <br>,\n * <img>, \u2026) need no closing tag.\n */\n toBeValidHTML(received) {\n const html = htmlOf(received);\n const error = typeof html === 'string' ? findHTMLError(html) : 'received no HTML';\n const pass = error === null;\n\n return {\n pass,\n message: () => pass\n ? 'Expected HTML not to be valid'\n : `Expected HTML to be valid, but ${error}`\n };\n }\n};\n\n/**\n * Extend expect with custom matchers\n *\n * @param {Object} expect - Expect function from testing framework\n *\n * @example\n * import { expect } from 'vitest';\n * import { extendExpect } from '@coherent.js/tooling/testing/matchers';\n *\n * extendExpect(expect);\n *\n * // Now you can use custom matchers\n * expect(element).toHaveText('Hello');\n */\nexport function extendExpect(expect) {\n if (expect && expect.extend) {\n expect.extend(customMatchers);\n } else {\n console.warn('Could not extend expect - expect.extend not available');\n }\n}\n\n/**\n * Create assertion helpers\n */\nexport const assertions = {\n /**\n * Assert element has text\n */\n assertHasText(element, text) {\n const actual = textOf(element);\n if (actual !== text) {\n throw new Error(`Expected element to have text \"${text}\", but got ${show(actual)}`);\n }\n },\n\n /**\n * Assert element exists\n */\n assertExists(element) {\n if (!element || !element.exists) {\n throw new Error('Expected element to exist');\n }\n },\n\n /**\n * Assert element has class (a whole class token)\n */\n assertHasClass(element, className) {\n if (!(classesOf(element) ?? []).includes(className)) {\n throw new Error(`Expected element to have class \"${className}\"`);\n }\n },\n\n /**\n * Assert HTML contains string\n */\n assertContainsHTML(html, substring) {\n const htmlString = htmlOf(html);\n if (!htmlString || !htmlString.includes(substring)) {\n throw new Error(`Expected HTML to contain \"${substring}\"`);\n }\n },\n\n /**\n * Assert component rendered\n */\n assertRendered(result) {\n if (!result || !result.html || result.html.length === 0) {\n throw new Error('Expected component to render');\n }\n }\n};\n\n/**\n * Export all matchers and utilities\n */\nexport default {\n customMatchers,\n extendExpect,\n assertions\n};\n"],
5
+ "mappings": ";AAsBA,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EAC5B;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAM;AAAA,EAAO;AAAA,EAAS;AAAA,EAAM;AAAA,EAAO;AAAA,EAAS;AAAA,EAC5D;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAU;AAAA,EAAS;AAC9C,CAAC;AAGD,IAAM,oBAAoB,oBAAI,IAAI,CAAC,UAAU,SAAS,YAAY,OAAO,CAAC;AAE1E,IAAM,iBAAiB,EAAE,KAAK,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,KAAK,MAAM,KAAK,MAAM,OAAI;AAErF,SAAS,eAAe,MAAM;AAC5B,SAAO,KAAK,QAAQ,iCAAiC,CAAC,QAAQ,SAAS;AACrE,QAAI,KAAK,CAAC,MAAM,KAAK;AACnB,YAAM,YAAY,KAAK,CAAC,MAAM,OAAO,KAAK,CAAC,MAAM,MAC7C,OAAO,SAAS,KAAK,MAAM,CAAC,GAAG,EAAE,IACjC,OAAO,SAAS,KAAK,MAAM,CAAC,GAAG,EAAE;AACrC,aAAO,aAAa,UAAW,OAAO,cAAc,SAAS,IAAI;AAAA,IACnE;AACA,WAAO,eAAe,KAAK,YAAY,CAAC,KAAK;AAAA,EAC/C,CAAC;AACH;AAGA,SAAS,OAAO,UAAU;AACxB,MAAI,OAAO,aAAa,SAAU,QAAO;AACzC,MAAI,YAAY,OAAO,SAAS,SAAS,SAAU,QAAO,SAAS;AACnE,SAAO;AACT;AAMA,SAAS,OAAO,UAAU;AACxB,MAAI,YAAY,OAAO,aAAa,YAAY,OAAO,SAAS,SAAS,UAAU;AACjF,WAAO,eAAe,SAAS,IAAI;AAAA,EACrC;AACA,QAAM,OAAO,OAAO,QAAQ;AAC5B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,eAAe,UAAU,IAAI,CAAC;AACvC;AAOA,SAAS,UAAU,MAAM;AACvB,MAAI,MAAM;AACV,MAAI,SAAS;AACb,SAAO,SAAS,KAAK,QAAQ;AAC3B,UAAM,OAAO,KAAK,QAAQ,KAAK,MAAM;AACrC,QAAI,SAAS,GAAI;AACjB,UAAM,QAAQ,KAAK,QAAQ,KAAK,OAAO,CAAC;AACxC,QAAI,UAAU,GAAI;AAClB,WAAO,KAAK,MAAM,QAAQ,IAAI;AAC9B,aAAS,QAAQ;AAAA,EACnB;AACA,SAAO,MAAM,KAAK,MAAM,MAAM;AAChC;AAEA,IAAM,UAAU,CAAC,OAAO,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAQ,OAAO,QAAQ,OAAO;AAQ1F,SAAS,gBAAgB,MAAM;AAC7B,QAAM,QAAQ,KAAK,OAAO,WAAW;AACrC,MAAI,UAAU,GAAI,QAAO;AAEzB,MAAI,IAAI,QAAQ;AAChB,MAAI,UAAU;AACd,SAAO,IAAI,KAAK,UAAU,SAAS,KAAK,KAAK,CAAC,CAAC,EAAG,YAAW,KAAK,GAAG;AAErE,QAAM,aAAa,oBAAI,IAAI;AAC3B,SAAO,IAAI,KAAK,QAAQ;AACtB,WAAO,IAAI,KAAK,UAAU,QAAQ,KAAK,CAAC,CAAC,EAAG;AAC5C,QAAI,KAAK,KAAK,UAAU,KAAK,CAAC,MAAM,IAAK;AACzC,QAAI,KAAK,CAAC,MAAM,KAAK;AACnB;AACA;AAAA,IACF;AAEA,QAAI,OAAO;AACX,WAAO,IAAI,KAAK,UAAU,CAAC,QAAQ,KAAK,CAAC,CAAC,KAAK,KAAK,CAAC,MAAM,OAAO,KAAK,CAAC,MAAM,OAAO,KAAK,CAAC,MAAM,KAAK;AACpG,cAAQ,KAAK,GAAG;AAAA,IAClB;AACA,WAAO,IAAI,KAAK,UAAU,QAAQ,KAAK,CAAC,CAAC,EAAG;AAE5C,QAAI,QAAQ;AACZ,QAAI,KAAK,CAAC,MAAM,KAAK;AACnB;AACA,aAAO,IAAI,KAAK,UAAU,QAAQ,KAAK,CAAC,CAAC,EAAG;AAC5C,YAAM,QAAQ,KAAK,CAAC;AACpB,UAAI,UAAU,OAAO,UAAU,KAAK;AAClC,cAAM,MAAM,KAAK,QAAQ,OAAO,IAAI,CAAC;AACrC,YAAI,QAAQ,GAAI;AAChB,gBAAQ,KAAK,MAAM,IAAI,GAAG,GAAG;AAC7B,YAAI,MAAM;AAAA,MACZ,OAAO;AACL,eAAO,IAAI,KAAK,UAAU,CAAC,QAAQ,KAAK,CAAC,CAAC,KAAK,KAAK,CAAC,MAAM,IAAK,UAAS,KAAK,GAAG;AAAA,MACnF;AAAA,IACF;AACA,QAAI,KAAM,YAAW,IAAI,KAAK,YAAY,GAAG,eAAe,KAAK,CAAC;AAAA,EACpE;AAEA,SAAO,EAAE,SAAS,QAAQ,YAAY,GAAG,WAAW;AACtD;AAGA,SAAS,UAAU,UAAU;AAC3B,QAAM,OAAO,OAAO,QAAQ;AAC5B,QAAM,MAAM,SAAS,OAAO,OAAO,gBAAgB,IAAI;AACvD,MAAI,QAAQ,KAAK,WAAW,IAAI,OAAO;AACvC,MAAI,UAAU,UAAa,OAAO,UAAU,cAAc,SAAU,SAAQ,SAAS;AACrF,SAAO,UAAU,SAAY,OAAO,MAAM,MAAM,KAAK,EAAE,OAAO,OAAO;AACvE;AAQA,SAAS,cAAc,MAAM;AAC3B,QAAM,QAAQ,CAAC;AACf,MAAI,QAAQ;AACZ,MAAI,IAAI;AAER,UAAQ,IAAI,KAAK,QAAQ,KAAK,CAAC,OAAO,IAAI;AACxC,QAAI,KAAK,WAAW,QAAQ,CAAC,GAAG;AAC9B,YAAM,MAAM,KAAK,QAAQ,OAAO,IAAI,CAAC;AACrC,UAAI,QAAQ,GAAI,QAAO;AACvB,UAAI,MAAM;AACV;AAAA,IACF;AAEA,UAAM,QAAQ,KAAK,QAAQ,KAAK,CAAC;AACjC,QAAI,UAAU,GAAI,QAAO;AACzB,UAAM,MAAM,KAAK,MAAM,IAAI,GAAG,KAAK;AACnC,QAAI,QAAQ;AAEZ,QAAI,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,MAAM,IAAK;AACtC,UAAM,QAAQ,0BAA0B,KAAK,GAAG;AAChD,QAAI,CAAC,MAAO;AACZ,UAAM,OAAO,MAAM,CAAC,EAAE,YAAY;AAElC,QAAI,MAAM,CAAC,GAAG;AACZ,UAAI,cAAc,IAAI,IAAI,EAAG,QAAO,KAAK,IAAI;AAC7C,YAAM,OAAO,MAAM,IAAI;AACvB,UAAI,SAAS,KAAM,QAAO,OAAO,KAAK,IAAI,qBAAqB,IAAI,MAAM,KAAK,IAAI;AAAA,IACpF,WAAW,CAAC,cAAc,IAAI,IAAI,KAAK,CAAC,IAAI,SAAS,GAAG,GAAG;AACzD,YAAM,KAAK,IAAI;AACf,UAAI,kBAAkB,IAAI,IAAI,GAAG;AAC/B,kBAAU,KAAK,YAAY;AAC3B,cAAM,MAAM,MAAM,QAAQ,KAAK,IAAI,IAAI,CAAC;AACxC,YAAI,QAAQ,GAAI,QAAO,IAAI,IAAI;AAC/B,YAAI;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEA,SAAO,MAAM,SAAS,IAAI,IAAI,MAAM,MAAM,SAAS,CAAC,CAAC,sBAAsB;AAC7E;AAEA,IAAM,OAAO,CAAC,UAAW,UAAU,QAAQ,UAAU,SAAY,YAAY,KAAK,UAAU,KAAK;AAK1F,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA,EAI5B,WAAW,UAAU,UAAU;AAC7B,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,OAAO,SAAS;AAEtB,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM,OACX,sCAAsC,QAAQ,MAC9C,kCAAkC,QAAQ,cAAc,KAAK,IAAI,CAAC;AAAA,IACxE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,UAAU,UAAU;AAChC,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,OAAO,OAAO,SAAS,YAAY,KAAK,SAAS,QAAQ;AAE/D,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM,OACX,yCAAyC,QAAQ,MACjD,qCAAqC,QAAQ,cAAc,KAAK,IAAI,CAAC;AAAA,IAC3E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,YAAY,UAAU,UAAU;AAC9B,UAAM,UAAU,UAAU,QAAQ,KAAK,CAAC;AACxC,UAAM,SAAS,OAAO,QAAQ,EAAE,MAAM,KAAK,EAAE,OAAO,OAAO;AAC3D,UAAM,OAAO,OAAO,SAAS,KAAK,OAAO,MAAM,CAAC,SAAS,QAAQ,SAAS,IAAI,CAAC;AAE/E,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM,OACX,uCAAuC,QAAQ,MAC/C,mCAAmC,QAAQ,0BAA0B,KAAK,QAAQ,KAAK,GAAG,CAAC,CAAC;AAAA,IAClG;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAkB,UAAU;AAC1B,UAAM,OAAO,QAAQ,YAAY,SAAS,WAAW,IAAI;AAEzD,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM,OACX,+CACA;AAAA,IACN;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,UAAU;AACpB,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,OAAO,OAAO,SAAS,YAAY,KAAK,KAAK,EAAE,SAAS;AAE9D,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM,OACX,uCACA;AAAA,IACN;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,UAAU;AAClB,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,OAAO,CAAC,QAAQ,KAAK,KAAK,EAAE,WAAW;AAE7C,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM,OACX,qCACA,iDAAiD,KAAK,IAAI,CAAC;AAAA,IACjE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,UAAU,UAAU;AAChC,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,OAAO,OAAO,SAAS,YAAY,KAAK,SAAS,QAAQ;AAE/D,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM,OACX,iCAAiC,QAAQ,MACzC,6BAA6B,QAAQ;AAAA,IAC3C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,UAAU,WAAW,OAAO;AAC1C,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,MAAM,SAAS,OAAO,OAAO,gBAAgB,IAAI;AACvD,UAAM,OAAO,OAAO,SAAS,EAAE,YAAY;AAC3C,UAAM,MAAM,QAAQ,KAAK,WAAW,IAAI,IAAI,CAAC;AAC7C,UAAM,SAAS,MAAM,IAAI,WAAW,IAAI,IAAI,IAAI;AAChD,UAAM,OAAO,UAAU,SAAY,OAAO,WAAW,OAAO,KAAK,IAAI;AAErE,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM;AACb,YAAI,UAAU,QAAW;AACvB,iBAAO,OACH,0CAA0C,SAAS,KAAK,KAAK,MAC7D,sCAAsC,SAAS,KAAK,KAAK,cAAc,MAAM,KAAK,MAAM,IAAI,MAAM;AAAA,QACxG;AACA,eAAO,OACH,0CAA0C,SAAS,KACnD,sCAAsC,SAAS;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,UAAU,SAAS;AAC/B,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,MAAM,SAAS,OAAO,OAAO,gBAAgB,IAAI;AACvD,UAAM,OAAO,QAAQ,GAAG,KAAK,IAAI,YAAY,OAAO,OAAO,EAAE,YAAY;AAEzE,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM,OACX,0CAA0C,OAAO,MACjD,sCAAsC,OAAO,cAAc,KAAK,KAAK,OAAO,CAAC;AAAA,IACnF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB,UAAU,SAAS;AAClC,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,cAAc,OAAO,OAAO;AAClC,UAAM,OAAO,OAAO,SAAS,YAAY,OAAO,gBAAgB,YAAY,KAAK,SAAS,WAAW;AAErG,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM,OACX,oCACA;AAAA,IACN;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,qBAAqB,UAAU;AAC7B,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,OAAO,OAAO,SAAS,YAAY,KAAK,SAAS;AAEvD,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM,OACX,kDACA;AAAA,IACN;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAc,UAAU;AACtB,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,QAAQ,OAAO,SAAS,WAAW,cAAc,IAAI,IAAI;AAC/D,UAAM,OAAO,UAAU;AAEvB,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MAAM,OACX,kCACA,kCAAkC,KAAK;AAAA,IAC7C;AAAA,EACF;AACF;AAgBO,SAAS,aAAa,QAAQ;AACnC,MAAI,UAAU,OAAO,QAAQ;AAC3B,WAAO,OAAO,cAAc;AAAA,EAC9B,OAAO;AACL,YAAQ,KAAK,uDAAuD;AAAA,EACtE;AACF;AAKO,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA,EAIxB,cAAc,SAAS,MAAM;AAC3B,UAAM,SAAS,OAAO,OAAO;AAC7B,QAAI,WAAW,MAAM;AACnB,YAAM,IAAI,MAAM,kCAAkC,IAAI,cAAc,KAAK,MAAM,CAAC,EAAE;AAAA,IACpF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,SAAS;AACpB,QAAI,CAAC,WAAW,CAAC,QAAQ,QAAQ;AAC/B,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,SAAS,WAAW;AACjC,QAAI,EAAE,UAAU,OAAO,KAAK,CAAC,GAAG,SAAS,SAAS,GAAG;AACnD,YAAM,IAAI,MAAM,mCAAmC,SAAS,GAAG;AAAA,IACjE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,MAAM,WAAW;AAClC,UAAM,aAAa,OAAO,IAAI;AAC9B,QAAI,CAAC,cAAc,CAAC,WAAW,SAAS,SAAS,GAAG;AAClD,YAAM,IAAI,MAAM,6BAA6B,SAAS,GAAG;AAAA,IAC3D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,QAAQ;AACrB,QAAI,CAAC,UAAU,CAAC,OAAO,QAAQ,OAAO,KAAK,WAAW,GAAG;AACvD,YAAM,IAAI,MAAM,8BAA8B;AAAA,IAChD;AAAA,EACF;AACF;AAKA,IAAO,mBAAQ;AAAA,EACb;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,8 @@
1
1
  // src/testing/test-renderer.js
2
2
  import { render } from "@coherent.js/core";
3
+ function escapeRegExp(text) {
4
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
+ }
3
6
  var TestRendererResult = class {
4
7
  constructor(component, html, container = null) {
5
8
  this.component = component;
@@ -13,7 +16,7 @@ var TestRendererResult = class {
13
16
  * @returns {Object|null} Element or null
14
17
  */
15
18
  getByTestId(testId) {
16
- const regex = new RegExp(`data-testid="${testId}"[^>]*>([^<]*)<`, "i");
19
+ const regex = new RegExp(`<[a-zA-Z][\\w:-]*(?:\\s[^>]*?)?\\sdata-testid="${escapeRegExp(testId)}"[^>]*>([^<]*)<`, "i");
17
20
  const match = this.html.match(regex);
18
21
  if (!match) {
19
22
  throw new Error(`Unable to find element with testId: ${testId}`);
@@ -43,7 +46,7 @@ var TestRendererResult = class {
43
46
  * @returns {Object} Element
44
47
  */
45
48
  getByText(text) {
46
- const regex = typeof text === "string" ? new RegExp(`>([^<]*${text}[^<]*)<`, "i") : new RegExp(`>([^<]*)<`, "i");
49
+ const regex = typeof text === "string" ? new RegExp(`>([^<]*${escapeRegExp(text)}[^<]*)<`, "i") : new RegExp(`>([^<]*)<`, "i");
47
50
  const match = this.html.match(regex);
48
51
  if (!match || typeof text === "string" && !match[1].includes(text)) {
49
52
  throw new Error(`Unable to find element with text: ${text}`);
@@ -72,7 +75,8 @@ var TestRendererResult = class {
72
75
  * @returns {Object} Element
73
76
  */
74
77
  getByClassName(className) {
75
- const regex = new RegExp(`class="[^"]*${className}[^"]*"[^>]*>([^<]*)<`, "i");
78
+ const token = escapeRegExp(className);
79
+ const regex = new RegExp(`<[a-zA-Z][\\w:-]*(?:\\s[^>]*?)?\\sclass="(?:[^"]*\\s)?${token}(?:\\s[^"]*)?"[^>]*>([^<]*)<`, "i");
76
80
  const match = this.html.match(regex);
77
81
  if (!match) {
78
82
  throw new Error(`Unable to find element with className: ${className}`);
@@ -102,7 +106,8 @@ var TestRendererResult = class {
102
106
  * @returns {Array<Object>} Array of elements
103
107
  */
104
108
  getAllByTagName(tagName) {
105
- const regex = new RegExp(`<${tagName}[^>]*>([^<]*)</${tagName}>`, "gi");
109
+ const tag = escapeRegExp(tagName);
110
+ const regex = new RegExp(`<${tag}(?=[\\s/>])[^>]*>([^<]*)</${tag}>`, "gi");
106
111
  const matches = [...this.html.matchAll(regex)];
107
112
  return matches.map((match) => ({
108
113
  text: match[1],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/testing/test-renderer.js"],
4
- "sourcesContent": ["/**\n * Coherent.js Test Renderer\n * \n * Provides utilities for rendering and testing Coherent.js components\n * in a test environment.\n * \n * @module testing/test-renderer\n */\n\nimport { render } from '@coherent.js/core';\n\n/**\n * Test renderer result\n * Provides methods to query and interact with rendered components\n */\nexport class TestRendererResult {\n constructor(component, html, container = null) {\n this.component = component;\n this.html = html;\n this.container = container;\n this.queries = new Map();\n }\n\n /**\n * Get element by test ID\n * @param {string} testId - Test ID to search for\n * @returns {Object|null} Element or null\n */\n getByTestId(testId) {\n const regex = new RegExp(`data-testid=\"${testId}\"[^>]*>([^<]*)<`, 'i');\n const match = this.html.match(regex);\n \n if (!match) {\n throw new Error(`Unable to find element with testId: ${testId}`);\n }\n \n return {\n text: match[1],\n html: match[0],\n testId,\n exists: true\n };\n }\n\n /**\n * Query element by test ID (returns null if not found)\n * @param {string} testId - Test ID to search for\n * @returns {Object|null} Element or null\n */\n queryByTestId(testId) {\n try {\n return this.getByTestId(testId);\n } catch {\n return null;\n }\n }\n\n /**\n * Get element by text content\n * @param {string|RegExp} text - Text to search for\n * @returns {Object} Element\n */\n getByText(text) {\n const regex = typeof text === 'string' \n ? new RegExp(`>([^<]*${text}[^<]*)<`, 'i')\n : new RegExp(`>([^<]*)<`, 'i');\n \n const match = this.html.match(regex);\n \n if (!match || (typeof text === 'string' && !match[1].includes(text))) {\n throw new Error(`Unable to find element with text: ${text}`);\n }\n \n return {\n text: match[1],\n html: match[0],\n exists: true\n };\n }\n\n /**\n * Query element by text (returns null if not found)\n * @param {string|RegExp} text - Text to search for\n * @returns {Object|null} Element or null\n */\n queryByText(text) {\n try {\n return this.getByText(text);\n } catch {\n return null;\n }\n }\n\n /**\n * Get element by class name\n * @param {string} className - Class name to search for\n * @returns {Object} Element\n */\n getByClassName(className) {\n const regex = new RegExp(`class=\"[^\"]*${className}[^\"]*\"[^>]*>([^<]*)<`, 'i');\n const match = this.html.match(regex);\n \n if (!match) {\n throw new Error(`Unable to find element with className: ${className}`);\n }\n \n return {\n text: match[1],\n html: match[0],\n className,\n exists: true\n };\n }\n\n /**\n * Query element by class name (returns null if not found)\n * @param {string} className - Class name to search for\n * @returns {Object|null} Element or null\n */\n queryByClassName(className) {\n try {\n return this.getByClassName(className);\n } catch {\n return null;\n }\n }\n\n /**\n * Get all elements by tag name\n * @param {string} tagName - Tag name to search for\n * @returns {Array<Object>} Array of elements\n */\n getAllByTagName(tagName) {\n const regex = new RegExp(`<${tagName}[^>]*>([^<]*)</${tagName}>`, 'gi');\n const matches = [...this.html.matchAll(regex)];\n \n return matches.map(match => ({\n text: match[1],\n html: match[0],\n tagName,\n exists: true\n }));\n }\n\n /**\n * Check if element exists\n * @param {string} selector - Selector (testId, text, className)\n * @param {string} type - Type of selector ('testId', 'text', 'className')\n * @returns {boolean} True if exists\n */\n exists(selector, type = 'testId') {\n switch (type) {\n case 'testId':\n return this.queryByTestId(selector) !== null;\n case 'text':\n return this.queryByText(selector) !== null;\n case 'className':\n return this.queryByClassName(selector) !== null;\n default:\n return false;\n }\n }\n\n /**\n * Get the rendered HTML\n * @returns {string} HTML string\n */\n getHTML() {\n return this.html;\n }\n\n /**\n * Get the component\n * @returns {Object} Component object\n */\n getComponent() {\n return this.component;\n }\n\n /**\n * Create a snapshot of the rendered output\n * @returns {string} Formatted HTML for snapshot testing\n */\n toSnapshot() {\n return this.html\n .replace(/>\\s+</g, '><') // Remove whitespace between tags\n .trim();\n }\n\n /**\n * Debug: print the rendered HTML\n */\n debug() {\n console.log('=== Rendered HTML ===');\n console.log(this.html);\n console.log('=== Component ===');\n console.log(JSON.stringify(this.component, null, 2));\n }\n}\n\n/**\n * Render a component for testing\n * \n * @param {Object} component - Component to render\n * @param {Object} [options] - Render options\n * @returns {TestRendererResult} Test renderer result\n * \n * @example\n * const { getByTestId } = renderComponent({\n * div: {\n * 'data-testid': 'my-div',\n * text: 'Hello World'\n * }\n * });\n * \n * expect(getByTestId('my-div').text).toBe('Hello World');\n */\nexport function renderComponent(component, options = {}) {\n const html = render(component, options);\n return new TestRendererResult(component, html);\n}\n\n/**\n * Render a component asynchronously\n * \n * @param {Object|Function} component - Component or component factory\n * @param {Object} [props] - Component props\n * @param {Object} [options] - Render options\n * @returns {Promise<TestRendererResult>} Test renderer result\n */\nexport async function renderComponentAsync(component, props = {}, options = {}) {\n // If component is a function, call it with props\n const resolvedComponent = typeof component === 'function' \n ? await component(props)\n : component;\n \n const html = render(resolvedComponent, options);\n return new TestRendererResult(resolvedComponent, html);\n}\n\n/**\n * Create a test renderer instance\n * Useful for testing component updates\n */\nexport class TestRenderer {\n constructor(component, options = {}) {\n this.component = component;\n this.options = options;\n this.result = null;\n this.renderCount = 0;\n }\n\n /**\n * Render the component\n * @returns {TestRendererResult} Render result\n */\n render() {\n this.renderCount++;\n const html = render(this.component, this.options);\n this.result = new TestRendererResult(this.component, html);\n return this.result;\n }\n\n /**\n * Update the component and re-render\n * @param {Object} newComponent - Updated component\n * @returns {TestRendererResult} Render result\n */\n update(newComponent) {\n this.component = newComponent;\n return this.render();\n }\n\n /**\n * Get the current result\n * @returns {TestRendererResult|null} Current result\n */\n getResult() {\n return this.result;\n }\n\n /**\n * Get render count\n * @returns {number} Number of renders\n */\n getRenderCount() {\n return this.renderCount;\n }\n\n /**\n * Unmount the component\n */\n unmount() {\n this.component = null;\n this.result = null;\n }\n}\n\n/**\n * Create a test renderer\n * \n * @param {Object} component - Component to render\n * @param {Object} [options] - Render options\n * @returns {TestRenderer} Test renderer instance\n * \n * @example\n * const renderer = createTestRenderer(MyComponent);\n * const result = renderer.render();\n * expect(result.getByText('Hello')).toBeTruthy();\n * \n * // Update and re-render\n * renderer.update(UpdatedComponent);\n * expect(renderer.getRenderCount()).toBe(2);\n */\nexport function createTestRenderer(component, options = {}) {\n return new TestRenderer(component, options);\n}\n\n/**\n * Shallow render a component (only render top level)\n * \n * @param {Object} component - Component to render\n * @returns {Object} Shallow rendered component\n */\nexport function shallowRender(component) {\n // Clone component without rendering children\n const shallow = { ...component };\n \n Object.keys(shallow).forEach(key => {\n if (shallow[key] && typeof shallow[key] === 'object') {\n if (shallow[key].children) {\n shallow[key] = {\n ...shallow[key],\n children: Array.isArray(shallow[key].children)\n ? shallow[key].children.map(() => ({ _shallow: true }))\n : { _shallow: true }\n };\n }\n }\n });\n \n return shallow;\n}\n\n/**\n * Export all testing utilities\n */\nexport default {\n renderComponent,\n renderComponentAsync,\n createTestRenderer,\n shallowRender,\n TestRenderer,\n TestRendererResult\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Coherent.js Test Renderer\n * \n * Provides utilities for rendering and testing Coherent.js components\n * in a test environment.\n * \n * @module testing/test-renderer\n */\n\nimport { render } from '@coherent.js/core';\n\n/** Escape a string for literal use inside a RegExp. */\nfunction escapeRegExp(text) {\n return String(text).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Test renderer result\n * Provides methods to query and interact with rendered components\n */\nexport class TestRendererResult {\n constructor(component, html, container = null) {\n this.component = component;\n this.html = html;\n this.container = container;\n this.queries = new Map();\n }\n\n /**\n * Get element by test ID\n * @param {string} testId - Test ID to search for\n * @returns {Object|null} Element or null\n */\n getByTestId(testId) {\n // Capture from the start of the opening tag, so the match's `html`\n // carries the element's tag name and all of its attributes.\n const regex = new RegExp(`<[a-zA-Z][\\\\w:-]*(?:\\\\s[^>]*?)?\\\\sdata-testid=\"${escapeRegExp(testId)}\"[^>]*>([^<]*)<`, 'i');\n const match = this.html.match(regex);\n \n if (!match) {\n throw new Error(`Unable to find element with testId: ${testId}`);\n }\n \n return {\n text: match[1],\n html: match[0],\n testId,\n exists: true\n };\n }\n\n /**\n * Query element by test ID (returns null if not found)\n * @param {string} testId - Test ID to search for\n * @returns {Object|null} Element or null\n */\n queryByTestId(testId) {\n try {\n return this.getByTestId(testId);\n } catch {\n return null;\n }\n }\n\n /**\n * Get element by text content\n * @param {string|RegExp} text - Text to search for\n * @returns {Object} Element\n */\n getByText(text) {\n const regex = typeof text === 'string' \n ? new RegExp(`>([^<]*${escapeRegExp(text)}[^<]*)<`, 'i')\n : new RegExp(`>([^<]*)<`, 'i');\n \n const match = this.html.match(regex);\n \n if (!match || (typeof text === 'string' && !match[1].includes(text))) {\n throw new Error(`Unable to find element with text: ${text}`);\n }\n \n return {\n text: match[1],\n html: match[0],\n exists: true\n };\n }\n\n /**\n * Query element by text (returns null if not found)\n * @param {string|RegExp} text - Text to search for\n * @returns {Object|null} Element or null\n */\n queryByText(text) {\n try {\n return this.getByText(text);\n } catch {\n return null;\n }\n }\n\n /**\n * Get element by class name\n * @param {string} className - Class name to search for\n * @returns {Object} Element\n */\n getByClassName(className) {\n // Whole class tokens: 'btn' matches class=\"btn primary\", not \"btn-primary\".\n const token = escapeRegExp(className);\n const regex = new RegExp(`<[a-zA-Z][\\\\w:-]*(?:\\\\s[^>]*?)?\\\\sclass=\"(?:[^\"]*\\\\s)?${token}(?:\\\\s[^\"]*)?\"[^>]*>([^<]*)<`, 'i');\n const match = this.html.match(regex);\n \n if (!match) {\n throw new Error(`Unable to find element with className: ${className}`);\n }\n \n return {\n text: match[1],\n html: match[0],\n className,\n exists: true\n };\n }\n\n /**\n * Query element by class name (returns null if not found)\n * @param {string} className - Class name to search for\n * @returns {Object|null} Element or null\n */\n queryByClassName(className) {\n try {\n return this.getByClassName(className);\n } catch {\n return null;\n }\n }\n\n /**\n * Get all elements by tag name\n * @param {string} tagName - Tag name to search for\n * @returns {Array<Object>} Array of elements\n */\n getAllByTagName(tagName) {\n const tag = escapeRegExp(tagName);\n const regex = new RegExp(`<${tag}(?=[\\\\s/>])[^>]*>([^<]*)</${tag}>`, 'gi');\n const matches = [...this.html.matchAll(regex)];\n \n return matches.map(match => ({\n text: match[1],\n html: match[0],\n tagName,\n exists: true\n }));\n }\n\n /**\n * Check if element exists\n * @param {string} selector - Selector (testId, text, className)\n * @param {string} type - Type of selector ('testId', 'text', 'className')\n * @returns {boolean} True if exists\n */\n exists(selector, type = 'testId') {\n switch (type) {\n case 'testId':\n return this.queryByTestId(selector) !== null;\n case 'text':\n return this.queryByText(selector) !== null;\n case 'className':\n return this.queryByClassName(selector) !== null;\n default:\n return false;\n }\n }\n\n /**\n * Get the rendered HTML\n * @returns {string} HTML string\n */\n getHTML() {\n return this.html;\n }\n\n /**\n * Get the component\n * @returns {Object} Component object\n */\n getComponent() {\n return this.component;\n }\n\n /**\n * Create a snapshot of the rendered output\n * @returns {string} Formatted HTML for snapshot testing\n */\n toSnapshot() {\n return this.html\n .replace(/>\\s+</g, '><') // Remove whitespace between tags\n .trim();\n }\n\n /**\n * Debug: print the rendered HTML\n */\n debug() {\n console.log('=== Rendered HTML ===');\n console.log(this.html);\n console.log('=== Component ===');\n console.log(JSON.stringify(this.component, null, 2));\n }\n}\n\n/**\n * Render a component for testing\n * \n * @param {Object} component - Component to render\n * @param {Object} [options] - Render options\n * @returns {TestRendererResult} Test renderer result\n * \n * @example\n * const { getByTestId } = renderComponent({\n * div: {\n * 'data-testid': 'my-div',\n * text: 'Hello World'\n * }\n * });\n * \n * expect(getByTestId('my-div').text).toBe('Hello World');\n */\nexport function renderComponent(component, options = {}) {\n const html = render(component, options);\n return new TestRendererResult(component, html);\n}\n\n/**\n * Render a component asynchronously\n * \n * @param {Object|Function} component - Component or component factory\n * @param {Object} [props] - Component props\n * @param {Object} [options] - Render options\n * @returns {Promise<TestRendererResult>} Test renderer result\n */\nexport async function renderComponentAsync(component, props = {}, options = {}) {\n // If component is a function, call it with props\n const resolvedComponent = typeof component === 'function' \n ? await component(props)\n : component;\n \n const html = render(resolvedComponent, options);\n return new TestRendererResult(resolvedComponent, html);\n}\n\n/**\n * Create a test renderer instance\n * Useful for testing component updates\n */\nexport class TestRenderer {\n constructor(component, options = {}) {\n this.component = component;\n this.options = options;\n this.result = null;\n this.renderCount = 0;\n }\n\n /**\n * Render the component\n * @returns {TestRendererResult} Render result\n */\n render() {\n this.renderCount++;\n const html = render(this.component, this.options);\n this.result = new TestRendererResult(this.component, html);\n return this.result;\n }\n\n /**\n * Update the component and re-render\n * @param {Object} newComponent - Updated component\n * @returns {TestRendererResult} Render result\n */\n update(newComponent) {\n this.component = newComponent;\n return this.render();\n }\n\n /**\n * Get the current result\n * @returns {TestRendererResult|null} Current result\n */\n getResult() {\n return this.result;\n }\n\n /**\n * Get render count\n * @returns {number} Number of renders\n */\n getRenderCount() {\n return this.renderCount;\n }\n\n /**\n * Unmount the component\n */\n unmount() {\n this.component = null;\n this.result = null;\n }\n}\n\n/**\n * Create a test renderer\n * \n * @param {Object} component - Component to render\n * @param {Object} [options] - Render options\n * @returns {TestRenderer} Test renderer instance\n * \n * @example\n * const renderer = createTestRenderer(MyComponent);\n * const result = renderer.render();\n * expect(result.getByText('Hello')).toBeTruthy();\n * \n * // Update and re-render\n * renderer.update(UpdatedComponent);\n * expect(renderer.getRenderCount()).toBe(2);\n */\nexport function createTestRenderer(component, options = {}) {\n return new TestRenderer(component, options);\n}\n\n/**\n * Shallow render a component (only render top level)\n * \n * @param {Object} component - Component to render\n * @returns {Object} Shallow rendered component\n */\nexport function shallowRender(component) {\n // Clone component without rendering children\n const shallow = { ...component };\n \n Object.keys(shallow).forEach(key => {\n if (shallow[key] && typeof shallow[key] === 'object') {\n if (shallow[key].children) {\n shallow[key] = {\n ...shallow[key],\n children: Array.isArray(shallow[key].children)\n ? shallow[key].children.map(() => ({ _shallow: true }))\n : { _shallow: true }\n };\n }\n }\n });\n \n return shallow;\n}\n\n/**\n * Export all testing utilities\n */\nexport default {\n renderComponent,\n renderComponentAsync,\n createTestRenderer,\n shallowRender,\n TestRenderer,\n TestRendererResult\n};\n"],
5
+ "mappings": ";AASA,SAAS,cAAc;AAGvB,SAAS,aAAa,MAAM;AAC1B,SAAO,OAAO,IAAI,EAAE,QAAQ,uBAAuB,MAAM;AAC3D;AAMO,IAAM,qBAAN,MAAyB;AAAA,EAC9B,YAAY,WAAW,MAAM,YAAY,MAAM;AAC7C,SAAK,YAAY;AACjB,SAAK,OAAO;AACZ,SAAK,YAAY;AACjB,SAAK,UAAU,oBAAI,IAAI;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAAY,QAAQ;AAGlB,UAAM,QAAQ,IAAI,OAAO,kDAAkD,aAAa,MAAM,CAAC,mBAAmB,GAAG;AACrH,UAAM,QAAQ,KAAK,KAAK,MAAM,KAAK;AAEnC,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,uCAAuC,MAAM,EAAE;AAAA,IACjE;AAEA,WAAO;AAAA,MACL,MAAM,MAAM,CAAC;AAAA,MACb,MAAM,MAAM,CAAC;AAAA,MACb;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,QAAQ;AACpB,QAAI;AACF,aAAO,KAAK,YAAY,MAAM;AAAA,IAChC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAU,MAAM;AACd,UAAM,QAAQ,OAAO,SAAS,WAC1B,IAAI,OAAO,UAAU,aAAa,IAAI,CAAC,WAAW,GAAG,IACrD,IAAI,OAAO,aAAa,GAAG;AAE/B,UAAM,QAAQ,KAAK,KAAK,MAAM,KAAK;AAEnC,QAAI,CAAC,SAAU,OAAO,SAAS,YAAY,CAAC,MAAM,CAAC,EAAE,SAAS,IAAI,GAAI;AACpE,YAAM,IAAI,MAAM,qCAAqC,IAAI,EAAE;AAAA,IAC7D;AAEA,WAAO;AAAA,MACL,MAAM,MAAM,CAAC;AAAA,MACb,MAAM,MAAM,CAAC;AAAA,MACb,QAAQ;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAAY,MAAM;AAChB,QAAI;AACF,aAAO,KAAK,UAAU,IAAI;AAAA,IAC5B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,eAAe,WAAW;AAExB,UAAM,QAAQ,aAAa,SAAS;AACpC,UAAM,QAAQ,IAAI,OAAO,yDAAyD,KAAK,gCAAgC,GAAG;AAC1H,UAAM,QAAQ,KAAK,KAAK,MAAM,KAAK;AAEnC,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,0CAA0C,SAAS,EAAE;AAAA,IACvE;AAEA,WAAO;AAAA,MACL,MAAM,MAAM,CAAC;AAAA,MACb,MAAM,MAAM,CAAC;AAAA,MACb;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,WAAW;AAC1B,QAAI;AACF,aAAO,KAAK,eAAe,SAAS;AAAA,IACtC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,gBAAgB,SAAS;AACvB,UAAM,MAAM,aAAa,OAAO;AAChC,UAAM,QAAQ,IAAI,OAAO,IAAI,GAAG,6BAA6B,GAAG,KAAK,IAAI;AACzE,UAAM,UAAU,CAAC,GAAG,KAAK,KAAK,SAAS,KAAK,CAAC;AAE7C,WAAO,QAAQ,IAAI,YAAU;AAAA,MAC3B,MAAM,MAAM,CAAC;AAAA,MACb,MAAM,MAAM,CAAC;AAAA,MACb;AAAA,MACA,QAAQ;AAAA,IACV,EAAE;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAO,UAAU,OAAO,UAAU;AAChC,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH,eAAO,KAAK,cAAc,QAAQ,MAAM;AAAA,MAC1C,KAAK;AACH,eAAO,KAAK,YAAY,QAAQ,MAAM;AAAA,MACxC,KAAK;AACH,eAAO,KAAK,iBAAiB,QAAQ,MAAM;AAAA,MAC7C;AACE,eAAO;AAAA,IACX;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU;AACR,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAAe;AACb,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa;AACX,WAAO,KAAK,KACT,QAAQ,UAAU,IAAI,EACtB,KAAK;AAAA,EACV;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ;AACN,YAAQ,IAAI,uBAAuB;AACnC,YAAQ,IAAI,KAAK,IAAI;AACrB,YAAQ,IAAI,mBAAmB;AAC/B,YAAQ,IAAI,KAAK,UAAU,KAAK,WAAW,MAAM,CAAC,CAAC;AAAA,EACrD;AACF;AAmBO,SAAS,gBAAgB,WAAW,UAAU,CAAC,GAAG;AACvD,QAAM,OAAO,OAAO,WAAW,OAAO;AACtC,SAAO,IAAI,mBAAmB,WAAW,IAAI;AAC/C;AAUA,eAAsB,qBAAqB,WAAW,QAAQ,CAAC,GAAG,UAAU,CAAC,GAAG;AAE9E,QAAM,oBAAoB,OAAO,cAAc,aAC3C,MAAM,UAAU,KAAK,IACrB;AAEJ,QAAM,OAAO,OAAO,mBAAmB,OAAO;AAC9C,SAAO,IAAI,mBAAmB,mBAAmB,IAAI;AACvD;AAMO,IAAM,eAAN,MAAmB;AAAA,EACxB,YAAY,WAAW,UAAU,CAAC,GAAG;AACnC,SAAK,YAAY;AACjB,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS;AACP,SAAK;AACL,UAAM,OAAO,OAAO,KAAK,WAAW,KAAK,OAAO;AAChD,SAAK,SAAS,IAAI,mBAAmB,KAAK,WAAW,IAAI;AACzD,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,cAAc;AACnB,SAAK,YAAY;AACjB,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,YAAY;AACV,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,iBAAiB;AACf,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU;AACR,SAAK,YAAY;AACjB,SAAK,SAAS;AAAA,EAChB;AACF;AAkBO,SAAS,mBAAmB,WAAW,UAAU,CAAC,GAAG;AAC1D,SAAO,IAAI,aAAa,WAAW,OAAO;AAC5C;AAQO,SAAS,cAAc,WAAW;AAEvC,QAAM,UAAU,EAAE,GAAG,UAAU;AAE/B,SAAO,KAAK,OAAO,EAAE,QAAQ,SAAO;AAClC,QAAI,QAAQ,GAAG,KAAK,OAAO,QAAQ,GAAG,MAAM,UAAU;AACpD,UAAI,QAAQ,GAAG,EAAE,UAAU;AACzB,gBAAQ,GAAG,IAAI;AAAA,UACb,GAAG,QAAQ,GAAG;AAAA,UACd,UAAU,MAAM,QAAQ,QAAQ,GAAG,EAAE,QAAQ,IACzC,QAAQ,GAAG,EAAE,SAAS,IAAI,OAAO,EAAE,UAAU,KAAK,EAAE,IACpD,EAAE,UAAU,KAAK;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO;AACT;AAKA,IAAO,wBAAQ;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": []
7
7
  }
@@ -112,6 +112,8 @@ function createMock(implementation) {
112
112
  implementation = () => Promise.reject(error);
113
113
  return mockFn;
114
114
  };
115
+ Object.defineProperty(mockFn, "_isMockFunction", { value: true });
116
+ mockFn.getMockName = () => "createMock()";
115
117
  return mockFn;
116
118
  }
117
119
  function createSpy(object, method) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/testing/test-utils.js"],
4
- "sourcesContent": ["/**\n * Coherent.js Test Utilities\n * \n * Helper functions for testing Coherent.js components\n * \n * @module testing/test-utils\n */\n\n/**\n * Simulate an event on an element\n * \n * @param {Object} element - Element to fire event on\n * @param {string} eventType - Type of event (click, change, etc.)\n * @param {Object} [eventData] - Additional event data\n */\nexport function fireEvent(element, eventType, eventData = {}) {\n if (!element) {\n throw new Error('Element is required for fireEvent');\n }\n \n // In a test environment, we simulate the event\n const event = {\n type: eventType,\n target: element,\n currentTarget: element,\n preventDefault: () => {},\n stopPropagation: () => {},\n ...eventData\n };\n \n // If element has an event handler, call it\n const handlerName = `on${eventType}`;\n if (element[handlerName] && typeof element[handlerName] === 'function') {\n element[handlerName](event);\n }\n \n return event;\n}\n\n/**\n * Common event helpers\n */\nexport const fireEvent_click = (element, eventData) => \n fireEvent(element, 'click', eventData);\n\nexport const fireEvent_change = (element, value) => \n fireEvent(element, 'change', { target: { value } });\n\nexport const fireEvent_input = (element, value) => \n fireEvent(element, 'input', { target: { value } });\n\nexport const fireEvent_submit = (element, eventData) => \n fireEvent(element, 'submit', eventData);\n\nexport const fireEvent_keyDown = (element, key) => \n fireEvent(element, 'keydown', { key });\n\nexport const fireEvent_keyUp = (element, key) => \n fireEvent(element, 'keyup', { key });\n\nexport const fireEvent_focus = (element) => \n fireEvent(element, 'focus');\n\nexport const fireEvent_blur = (element) => \n fireEvent(element, 'blur');\n\n/**\n * Wait for a condition to be true\n * \n * @param {Function} condition - Condition function\n * @param {Object} [options] - Wait options\n * @param {number} [options.timeout=1000] - Timeout in ms\n * @param {number} [options.interval=50] - Check interval in ms\n * @returns {Promise<void>}\n * \n * @example\n * await waitFor(() => getByText('Loaded').exists, { timeout: 2000 });\n */\nexport function waitFor(condition, options = {}) {\n const { timeout = 1000, interval = 50 } = options;\n \n return new Promise((resolve, reject) => {\n const startTime = Date.now();\n \n const check = () => {\n try {\n if (condition()) {\n resolve();\n return;\n }\n } catch {\n // Condition threw an error, keep waiting\n }\n \n if (Date.now() - startTime >= timeout) {\n reject(new Error(`Timeout waiting for condition after ${timeout}ms`));\n return;\n }\n \n setTimeout(check, interval);\n };\n \n check();\n });\n}\n\n/**\n * Wait for element to appear\n * \n * @param {Function} queryFn - Query function that returns element\n * @param {Object} [options] - Wait options\n * @returns {Promise<Object>} Element\n */\nexport async function waitForElement(queryFn, options = {}) {\n let element = null;\n \n await waitFor(() => {\n element = queryFn();\n return element !== null;\n }, options);\n \n return element;\n}\n\n/**\n * Wait for element to disappear\n * \n * @param {Function} queryFn - Query function that returns element\n * @param {Object} [options] - Wait options\n * @returns {Promise<void>}\n */\nexport async function waitForElementToBeRemoved(queryFn, options = {}) {\n await waitFor(() => {\n const element = queryFn();\n return element === null;\n }, options);\n}\n\n/**\n * Act utility for batching updates\n * Useful for testing state changes\n * \n * @param {Function} callback - Callback to execute\n * @returns {Promise<void>}\n */\nexport async function act(callback) {\n await callback();\n // Allow any pending updates to flush\n await new Promise(resolve => setTimeout(resolve, 0));\n}\n\n/**\n * Create a mock function\n * \n * @param {Function} [implementation] - Optional implementation\n * @returns {Function} Mock function\n */\nexport function createMock(implementation) {\n const calls = [];\n const results = [];\n \n const mockFn = function(...args) {\n calls.push(args);\n \n let result;\n let error;\n \n try {\n result = implementation ? implementation(...args) : undefined;\n results.push({ type: 'return', value: result });\n } catch (err) {\n error = err;\n results.push({ type: 'throw', value: error });\n throw error;\n }\n \n return result;\n };\n \n // Add mock utilities\n mockFn.mock = {\n calls,\n results,\n instances: []\n };\n \n mockFn.mockClear = () => {\n calls.length = 0;\n results.length = 0;\n };\n \n mockFn.mockReset = () => {\n mockFn.mockClear();\n implementation = undefined;\n };\n \n mockFn.mockImplementation = (fn) => {\n implementation = fn;\n return mockFn;\n };\n \n mockFn.mockReturnValue = (value) => {\n implementation = () => value;\n return mockFn;\n };\n \n mockFn.mockResolvedValue = (value) => {\n implementation = () => Promise.resolve(value);\n return mockFn;\n };\n \n mockFn.mockRejectedValue = (error) => {\n implementation = () => Promise.reject(error);\n return mockFn;\n };\n \n return mockFn;\n}\n\n/**\n * Create a spy on an object method\n * \n * @param {Object} object - Object to spy on\n * @param {string} method - Method name\n * @returns {Function} Spy function\n */\nexport function createSpy(object, method) {\n const original = object[method];\n const spy = createMock(original.bind(object));\n \n object[method] = spy;\n \n spy.mockRestore = () => {\n object[method] = original;\n };\n \n return spy;\n}\n\n/**\n * Cleanup utility\n * Cleans up after tests\n */\nexport function cleanup() {\n // Clear any timers\n // Reset any global state\n // This would be expanded based on framework needs\n}\n\n/**\n * Within utility - scopes queries to a container\n * \n * @param {Object} container - Container result\n * @returns {Object} Scoped queries\n */\nexport function within(container) {\n return {\n getByTestId: (testId) => container.getByTestId(testId),\n queryByTestId: (testId) => container.queryByTestId(testId),\n getByText: (text) => container.getByText(text),\n queryByText: (text) => container.queryByText(text),\n getByClassName: (className) => container.getByClassName(className),\n queryByClassName: (className) => container.queryByClassName(className)\n };\n}\n\n/**\n * Screen utility - global queries\n * Useful for accessing rendered content without storing result\n */\nexport const screen = {\n _result: null,\n \n setResult(result) {\n this._result = result;\n },\n \n getByTestId(testId) {\n if (!this._result) throw new Error('No component rendered');\n return this._result.getByTestId(testId);\n },\n \n queryByTestId(testId) {\n if (!this._result) return null;\n return this._result.queryByTestId(testId);\n },\n \n getByText(text) {\n if (!this._result) throw new Error('No component rendered');\n return this._result.getByText(text);\n },\n \n queryByText(text) {\n if (!this._result) return null;\n return this._result.queryByText(text);\n },\n \n getByClassName(className) {\n if (!this._result) throw new Error('No component rendered');\n return this._result.getByClassName(className);\n },\n \n queryByClassName(className) {\n if (!this._result) return null;\n return this._result.queryByClassName(className);\n },\n \n debug() {\n if (this._result) {\n this._result.debug();\n }\n }\n};\n\n/**\n * User event simulation\n * More realistic event simulation than fireEvent\n */\nexport const userEvent = {\n /**\n * Simulate user typing\n */\n type: async (element, text, options = {}) => {\n const { delay = 0 } = options;\n \n for (const char of text) {\n fireEvent_keyDown(element, char);\n fireEvent_input(element, element.value + char);\n fireEvent_keyUp(element, char);\n \n if (delay > 0) {\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n },\n \n /**\n * Simulate user click\n */\n click: async (element) => {\n fireEvent_focus(element);\n fireEvent_click(element);\n },\n \n /**\n * Simulate user double click\n */\n dblClick: async (element) => {\n await userEvent.click(element);\n await userEvent.click(element);\n },\n \n /**\n * Simulate user clearing input\n */\n clear: async (element) => {\n fireEvent_input(element, '');\n fireEvent_change(element, '');\n },\n \n /**\n * Simulate user selecting option\n */\n selectOptions: async (element, values) => {\n const valueArray = Array.isArray(values) ? values : [values];\n fireEvent_change(element, valueArray[0]);\n },\n \n /**\n * Simulate user tab navigation\n */\n tab: async () => {\n // Simulate tab key\n const activeElement = document.activeElement;\n if (activeElement) {\n fireEvent_keyDown(activeElement, 'Tab');\n fireEvent_blur(activeElement);\n }\n }\n};\n\n/**\n * Export all utilities\n */\nexport default {\n fireEvent,\n waitFor,\n waitForElement,\n waitForElementToBeRemoved,\n act,\n createMock,\n createSpy,\n cleanup,\n within,\n screen,\n userEvent\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Coherent.js Test Utilities\n * \n * Helper functions for testing Coherent.js components\n * \n * @module testing/test-utils\n */\n\n/**\n * Simulate an event on an element\n * \n * @param {Object} element - Element to fire event on\n * @param {string} eventType - Type of event (click, change, etc.)\n * @param {Object} [eventData] - Additional event data\n */\nexport function fireEvent(element, eventType, eventData = {}) {\n if (!element) {\n throw new Error('Element is required for fireEvent');\n }\n \n // In a test environment, we simulate the event\n const event = {\n type: eventType,\n target: element,\n currentTarget: element,\n preventDefault: () => {},\n stopPropagation: () => {},\n ...eventData\n };\n \n // If element has an event handler, call it\n const handlerName = `on${eventType}`;\n if (element[handlerName] && typeof element[handlerName] === 'function') {\n element[handlerName](event);\n }\n \n return event;\n}\n\n/**\n * Common event helpers\n */\nexport const fireEvent_click = (element, eventData) => \n fireEvent(element, 'click', eventData);\n\nexport const fireEvent_change = (element, value) => \n fireEvent(element, 'change', { target: { value } });\n\nexport const fireEvent_input = (element, value) => \n fireEvent(element, 'input', { target: { value } });\n\nexport const fireEvent_submit = (element, eventData) => \n fireEvent(element, 'submit', eventData);\n\nexport const fireEvent_keyDown = (element, key) => \n fireEvent(element, 'keydown', { key });\n\nexport const fireEvent_keyUp = (element, key) => \n fireEvent(element, 'keyup', { key });\n\nexport const fireEvent_focus = (element) => \n fireEvent(element, 'focus');\n\nexport const fireEvent_blur = (element) => \n fireEvent(element, 'blur');\n\n/**\n * Wait for a condition to be true\n * \n * @param {Function} condition - Condition function\n * @param {Object} [options] - Wait options\n * @param {number} [options.timeout=1000] - Timeout in ms\n * @param {number} [options.interval=50] - Check interval in ms\n * @returns {Promise<void>}\n * \n * @example\n * await waitFor(() => getByText('Loaded').exists, { timeout: 2000 });\n */\nexport function waitFor(condition, options = {}) {\n const { timeout = 1000, interval = 50 } = options;\n \n return new Promise((resolve, reject) => {\n const startTime = Date.now();\n \n const check = () => {\n try {\n if (condition()) {\n resolve();\n return;\n }\n } catch {\n // Condition threw an error, keep waiting\n }\n \n if (Date.now() - startTime >= timeout) {\n reject(new Error(`Timeout waiting for condition after ${timeout}ms`));\n return;\n }\n \n setTimeout(check, interval);\n };\n \n check();\n });\n}\n\n/**\n * Wait for element to appear\n * \n * @param {Function} queryFn - Query function that returns element\n * @param {Object} [options] - Wait options\n * @returns {Promise<Object>} Element\n */\nexport async function waitForElement(queryFn, options = {}) {\n let element = null;\n \n await waitFor(() => {\n element = queryFn();\n return element !== null;\n }, options);\n \n return element;\n}\n\n/**\n * Wait for element to disappear\n * \n * @param {Function} queryFn - Query function that returns element\n * @param {Object} [options] - Wait options\n * @returns {Promise<void>}\n */\nexport async function waitForElementToBeRemoved(queryFn, options = {}) {\n await waitFor(() => {\n const element = queryFn();\n return element === null;\n }, options);\n}\n\n/**\n * Act utility for batching updates\n * Useful for testing state changes\n * \n * @param {Function} callback - Callback to execute\n * @returns {Promise<void>}\n */\nexport async function act(callback) {\n await callback();\n // Allow any pending updates to flush\n await new Promise(resolve => setTimeout(resolve, 0));\n}\n\n/**\n * Create a mock function\n * \n * @param {Function} [implementation] - Optional implementation\n * @returns {Function} Mock function\n */\nexport function createMock(implementation) {\n const calls = [];\n const results = [];\n \n const mockFn = function(...args) {\n calls.push(args);\n \n let result;\n let error;\n \n try {\n result = implementation ? implementation(...args) : undefined;\n results.push({ type: 'return', value: result });\n } catch (err) {\n error = err;\n results.push({ type: 'throw', value: error });\n throw error;\n }\n \n return result;\n };\n \n // Add mock utilities\n mockFn.mock = {\n calls,\n results,\n instances: []\n };\n \n mockFn.mockClear = () => {\n calls.length = 0;\n results.length = 0;\n };\n \n mockFn.mockReset = () => {\n mockFn.mockClear();\n implementation = undefined;\n };\n \n mockFn.mockImplementation = (fn) => {\n implementation = fn;\n return mockFn;\n };\n \n mockFn.mockReturnValue = (value) => {\n implementation = () => value;\n return mockFn;\n };\n \n mockFn.mockResolvedValue = (value) => {\n implementation = () => Promise.resolve(value);\n return mockFn;\n };\n \n mockFn.mockRejectedValue = (error) => {\n implementation = () => Promise.reject(error);\n return mockFn;\n };\n\n // Mark it the way Vitest and Jest recognise mocks, so their built-in\n // toHaveBeenCalled / toHaveBeenCalledWith / toHaveBeenCalledTimes\n // (deep-equality argument matching) work on it.\n Object.defineProperty(mockFn, '_isMockFunction', { value: true });\n mockFn.getMockName = () => 'createMock()';\n\n return mockFn;\n}\n\n/**\n * Create a spy on an object method\n * \n * @param {Object} object - Object to spy on\n * @param {string} method - Method name\n * @returns {Function} Spy function\n */\nexport function createSpy(object, method) {\n const original = object[method];\n const spy = createMock(original.bind(object));\n \n object[method] = spy;\n \n spy.mockRestore = () => {\n object[method] = original;\n };\n \n return spy;\n}\n\n/**\n * Cleanup utility\n * Cleans up after tests\n */\nexport function cleanup() {\n // Clear any timers\n // Reset any global state\n // This would be expanded based on framework needs\n}\n\n/**\n * Within utility - scopes queries to a container\n * \n * @param {Object} container - Container result\n * @returns {Object} Scoped queries\n */\nexport function within(container) {\n return {\n getByTestId: (testId) => container.getByTestId(testId),\n queryByTestId: (testId) => container.queryByTestId(testId),\n getByText: (text) => container.getByText(text),\n queryByText: (text) => container.queryByText(text),\n getByClassName: (className) => container.getByClassName(className),\n queryByClassName: (className) => container.queryByClassName(className)\n };\n}\n\n/**\n * Screen utility - global queries\n * Useful for accessing rendered content without storing result\n */\nexport const screen = {\n _result: null,\n \n setResult(result) {\n this._result = result;\n },\n \n getByTestId(testId) {\n if (!this._result) throw new Error('No component rendered');\n return this._result.getByTestId(testId);\n },\n \n queryByTestId(testId) {\n if (!this._result) return null;\n return this._result.queryByTestId(testId);\n },\n \n getByText(text) {\n if (!this._result) throw new Error('No component rendered');\n return this._result.getByText(text);\n },\n \n queryByText(text) {\n if (!this._result) return null;\n return this._result.queryByText(text);\n },\n \n getByClassName(className) {\n if (!this._result) throw new Error('No component rendered');\n return this._result.getByClassName(className);\n },\n \n queryByClassName(className) {\n if (!this._result) return null;\n return this._result.queryByClassName(className);\n },\n \n debug() {\n if (this._result) {\n this._result.debug();\n }\n }\n};\n\n/**\n * User event simulation\n * More realistic event simulation than fireEvent\n */\nexport const userEvent = {\n /**\n * Simulate user typing\n */\n type: async (element, text, options = {}) => {\n const { delay = 0 } = options;\n \n for (const char of text) {\n fireEvent_keyDown(element, char);\n fireEvent_input(element, element.value + char);\n fireEvent_keyUp(element, char);\n \n if (delay > 0) {\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n },\n \n /**\n * Simulate user click\n */\n click: async (element) => {\n fireEvent_focus(element);\n fireEvent_click(element);\n },\n \n /**\n * Simulate user double click\n */\n dblClick: async (element) => {\n await userEvent.click(element);\n await userEvent.click(element);\n },\n \n /**\n * Simulate user clearing input\n */\n clear: async (element) => {\n fireEvent_input(element, '');\n fireEvent_change(element, '');\n },\n \n /**\n * Simulate user selecting option\n */\n selectOptions: async (element, values) => {\n const valueArray = Array.isArray(values) ? values : [values];\n fireEvent_change(element, valueArray[0]);\n },\n \n /**\n * Simulate user tab navigation\n */\n tab: async () => {\n // Simulate tab key\n const activeElement = document.activeElement;\n if (activeElement) {\n fireEvent_keyDown(activeElement, 'Tab');\n fireEvent_blur(activeElement);\n }\n }\n};\n\n/**\n * Export all utilities\n */\nexport default {\n fireEvent,\n waitFor,\n waitForElement,\n waitForElementToBeRemoved,\n act,\n createMock,\n createSpy,\n cleanup,\n within,\n screen,\n userEvent\n};\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coherent.js/tooling",
3
- "version": "1.1.0",
3
+ "version": "2.0.0-rc.0",
4
4
  "description": "Coherent.js dev-time tooling: testing utilities (Vitest matchers, render harness) and Language Server Protocol implementation.",
5
5
  "type": "module",
6
6
  "main": "./dist/testing/index.js",
@@ -32,7 +32,7 @@
32
32
  }
33
33
  },
34
34
  "bin": {
35
- "coherent-language-server": "./dist/lsp/server.js"
35
+ "coherent-language-server": "./dist/lsp/bin.js"
36
36
  },
37
37
  "files": [
38
38
  "dist/",
@@ -64,16 +64,17 @@
64
64
  "url": "https://github.com/Tomdrouv1/coherent.js/issues"
65
65
  },
66
66
  "peerDependencies": {
67
- "@coherent.js/core": "^1.1.0"
67
+ "@coherent.js/core": "^2.0.0-rc.0"
68
68
  },
69
69
  "dependencies": {
70
70
  "typescript": "^5.9.3",
71
- "vscode-languageserver": "10.1.0",
72
- "vscode-languageserver-textdocument": "1.0.12"
71
+ "vscode-languageserver": "10.1.1",
72
+ "vscode-languageserver-textdocument": "1.0.14"
73
73
  },
74
74
  "devDependencies": {
75
- "tsx": "4.23.1",
76
- "vitest": "4.1.10"
75
+ "@types/node": "26.6.1",
76
+ "tsx": "4.23.13",
77
+ "vitest": "5.0.0"
77
78
  },
78
79
  "publishConfig": {
79
80
  "access": "public",
@@ -87,7 +88,7 @@
87
88
  "build": "node build.mjs",
88
89
  "clean": "rm -rf dist",
89
90
  "dev": "tsc -w",
90
- "start": "node dist/lsp/server.js --stdio",
91
+ "start": "node dist/lsp/bin.js --stdio",
91
92
  "extract-attributes": "tsx scripts/extract-attributes.ts",
92
93
  "test": "vitest run",
93
94
  "test:watch": "vitest",
@@ -111,67 +111,74 @@ export class TestRenderer {
111
111
  * Render a component for testing
112
112
  */
113
113
  export function renderComponent(
114
- component: CoherentComponent | CoherentNode,
115
- props?: Record<string, unknown>
116
- ): RenderResult;
114
+ component: CoherentNode,
115
+ options?: Record<string, unknown>
116
+ ): TestRendererResult;
117
117
 
118
118
  /**
119
- * Render a component asynchronously
119
+ * Render a component (or a possibly-async component function called with
120
+ * `props`) for testing
120
121
  */
121
122
  export function renderComponentAsync(
122
- component: CoherentNode,
123
- options?: RenderOptions
124
- ): Promise<RenderResult>;
123
+ component: CoherentComponent | CoherentNode,
124
+ props?: Record<string, unknown>,
125
+ options?: Record<string, unknown>
126
+ ): Promise<TestRendererResult>;
125
127
 
126
128
  /**
127
129
  * Create a new test renderer instance
128
130
  */
129
- export function createTestRenderer(): TestRenderer;
131
+ export function createTestRenderer(component: CoherentNode, options?: RenderOptions): TestRenderer;
130
132
 
131
133
  /**
132
- * Shallow render a component
134
+ * Shallow render a component: children are replaced by `{ _shallow: true }` placeholders
133
135
  */
134
- export function shallowRender(component: CoherentNode): RenderResult;
136
+ export function shallowRender(component: CoherentNode): CoherentNode;
135
137
 
136
138
  // ============================================================================
137
139
  // Custom Matchers for Coherent.js
138
140
  // ============================================================================
139
141
 
140
142
  /**
141
- * Coherent.js-specific test matchers
143
+ * Matchers registered by `extendExpect(expect)`. Each accepts a
144
+ * `renderComponent()` result, a query match (`getByTestId()` …) or an HTML
145
+ * string. Element matchers look at the first element in that HTML.
146
+ *
147
+ * None of them shadows a Vitest/Jest built-in: snapshot with
148
+ * `expect(result.toSnapshot()).toMatchSnapshot()`, and use the built-in
149
+ * `toHaveBeenCalled*` matchers for `vi.fn()` or `createMock()` mocks.
142
150
  */
143
151
  export interface CoherentMatchers<R = unknown> {
144
- // Element structure matchers
145
- /** Assert element has specific tag name */
146
- toHaveTag(tagName: string): R;
147
- /** Assert element contains text */
152
+ /** Text content (entities decoded) equals `text` */
148
153
  toHaveText(text: string): R;
149
- /** Assert element has attribute (optionally with value) */
154
+ /** Text content (entities decoded) contains `text` */
155
+ toContainText(text: string): R;
156
+ /** The element has every given class, as whole tokens ('btn' ≠ 'btn-primary') */
157
+ toHaveClass(className: string): R;
158
+ /** A query match that found its element */
159
+ toBeInTheDocument(): R;
160
+ /** Has non-whitespace text content */
161
+ toBeVisible(): R;
162
+ /** Has no text content */
163
+ toBeEmpty(): R;
164
+ /** The HTML contains `html` verbatim */
165
+ toContainHTML(html: string): R;
166
+ /** The element has attribute `name` (with exactly `value`, when given) */
150
167
  toHaveAttribute(name: string, value?: string): R;
151
- /** Assert element has CSS class */
152
- toHaveClassName(className: string): R;
153
- /** Assert element has children (optionally specific count) */
154
- toHaveChildren(count?: number): R;
155
-
156
- // Component matchers
157
- /** Assert component renders an element with tag */
158
- toRenderElement(tagName: string): R;
159
- /** Assert component renders text content */
160
- toRenderText(text: string): R;
161
- /** Assert component matches snapshot */
162
- toMatchComponentSnapshot(): R;
163
-
164
- // Hydration matchers
165
- /** Assert hydration completes without mismatch */
166
- toHydrateWithoutMismatch(): R;
167
- /** Assert hydrated component has specific state */
168
- toHaveState(state: Record<string, unknown>): R;
169
-
170
- // Accessibility matchers
171
- /** Assert element has accessible name */
172
- toHaveAccessibleName(name: string): R;
173
- /** Assert element has ARIA role */
174
- toHaveRole(role: string): R;
168
+ /** The element's tag name is `tagName` */
169
+ toHaveTagName(tagName: string): R;
170
+ /** The HTML contains the given element's HTML */
171
+ toContainElement(element: string | { html?: string }): R;
172
+ /** Rendering produced non-empty HTML */
173
+ toRenderSuccessfully(): R;
174
+ /** Every non-void tag is closed, in order */
175
+ toBeValidHTML(): R;
176
+ }
177
+
178
+ /** Result a matcher implementation returns to `expect.extend()` */
179
+ export interface MatcherResult {
180
+ pass: boolean;
181
+ message: () => string;
175
182
  }
176
183
 
177
184
  // ============================================================================
@@ -346,40 +353,17 @@ export const assertions: {
346
353
  assertRendered(result: { html?: string } | null): void;
347
354
  };
348
355
 
349
- // ============================================================================
350
- // DOM Matchers (for Vitest/Jest)
351
- // ============================================================================
352
-
353
356
  /**
354
- * Custom DOM matchers
357
+ * The matcher implementations, keyed by name, for `expect.extend()`
355
358
  */
356
- export interface CustomMatchers<R = void> {
357
- toHaveHTML(html: string): R;
358
- toContainHTML(html: string): R;
359
- toHaveTextContent(text: string | RegExp): R;
360
- toHaveAttribute(attr: string, value?: string): R;
361
- toHaveClass(className: string): R;
362
- toBeInTheDocument(): R;
363
- toBeVisible(): R;
364
- toBeDisabled(): R;
365
- toBeEnabled(): R;
366
- toHaveValue(value: unknown): R;
367
- toHaveStyle(style: Record<string, unknown>): R;
368
- toHaveFocus(): R;
369
- toBeChecked(): R;
370
- toBeValid(): R;
371
- toBeInvalid(): R;
372
- }
373
-
374
- /**
375
- * Custom matchers object
376
- */
377
- export const customMatchers: CustomMatchers;
359
+ export const customMatchers: {
360
+ [K in keyof CoherentMatchers]: (received: unknown, ...args: any[]) => MatcherResult;
361
+ };
378
362
 
379
363
  /**
380
- * Extend test framework expect
364
+ * Register {@link customMatchers} on a framework's `expect` (Vitest, Jest)
381
365
  */
382
- export function extendExpect(matchers: Record<string, (...args: unknown[]) => unknown>): void;
366
+ export function extendExpect(expect: { extend(matchers: Record<string, unknown>): void }): void;
383
367
 
384
368
  // ============================================================================
385
369
  // Vitest/Jest Module Extensions
@@ -387,18 +371,18 @@ export function extendExpect(matchers: Record<string, (...args: unknown[]) => un
387
371
 
388
372
  // Extend Vitest matchers
389
373
  declare module 'vitest' {
390
- interface Assertion<T = unknown> extends CoherentMatchers<T>, CustomMatchers<T> {}
391
- interface AsymmetricMatchersContaining extends CoherentMatchers, CustomMatchers {}
374
+ interface Assertion<T = unknown> extends CoherentMatchers<T> {}
375
+ interface AsymmetricMatchersContaining extends CoherentMatchers {}
392
376
  }
393
377
 
394
378
  // Extend Jest matchers (for users using Jest)
395
379
  declare global {
396
380
  namespace Vi {
397
- interface Matchers<R = void> extends CustomMatchers<R>, CoherentMatchers<R> {}
398
- interface AsymmetricMatchers extends CustomMatchers, CoherentMatchers {}
381
+ interface Matchers<R = void> extends CoherentMatchers<R> {}
382
+ interface AsymmetricMatchers extends CoherentMatchers {}
399
383
  }
400
384
  namespace jest {
401
- interface Matchers<R = void> extends CustomMatchers<R>, CoherentMatchers<R> {}
402
- interface Expect extends CustomMatchers, CoherentMatchers {}
385
+ interface Matchers<R = void> extends CoherentMatchers<R> {}
386
+ interface Expect extends CoherentMatchers {}
403
387
  }
404
388
  }