packages/testing/src/specialized-assertions.ts
import { CustomElement } from '@aurelia/runtime-html';
import { InstructionType } from '@aurelia/template-compiler';
import { assert } from './assert';
// Disabling this as it this is nowhere used. And also the ast-serialization infra is moved to validation package.
// export function verifyASTEqual(actual: any, expected: any, errors?: string[], path?: string): any {
// if (expected == null) {
// if (actual != null) {
// assert.strictEqual(actual, null, `actual`);
// }
// } else if (actual == null) {
// const expectedSerialized = Serializer.serialize(expected);
// assert.strictEqual(actual, expectedSerialized, `actual`);
// } else {
// const expectedSerialized = Serializer.serialize(expected);
// const expectedUnparsed = Unparser.unparse(expected);
// const actualSerialized = Serializer.serialize(actual);
// const actualUnparsed = Unparser.unparse(actual);
// if (actualSerialized !== expectedSerialized) {
// assert.strictEqual(actualSerialized, expectedSerialized, `actualSerialized`);
// }
// if (actualUnparsed !== expectedUnparsed) {
// assert.strictEqual(actualUnparsed, expectedUnparsed, `actualUnparsed`);
// }
// }
// }
export function verifyEqual(actual: any, expected: any, depth?: number, property?: string, index?: number): any {
if (depth === undefined) {
depth = 0;
}
if (typeof expected !== 'object' || expected === null) {
assert.strictEqual(actual, expected, `actual, depth=${depth}, prop=${property}, index=${index}`);
return;
}
if (expected instanceof Array) {
for (let i = 0; i < expected.length; i++) {
verifyEqual(actual[i], expected[i], depth + 1, property, i);
}
return;
}
if (expected.nodeType > 0) {
if (expected.nodeType === 11) {
for (let i = 0; i < expected.childNodes.length; i++) {
verifyEqual(actual.childNodes.item(i), expected.childNodes.item(i), depth + 1, property, i);
}
} else {
assert.strictEqual(actual.outerHTML, expected.outerHTML, `actual.outerHTML, depth=${depth}, prop=${property}, index=${index}`);
}
return;
}
if (actual) {
assert.strictEqual(actual.constructor.name, expected.constructor.name, `actual.constructor.name, depth=${depth}, prop=${property}, index=${index}`);
assert.strictEqual(actual.toString(), expected.toString(), `actual.toString(), depth=${depth}, prop=${property}, index=${index}`);
for (const prop of Object.keys(expected)) {
verifyEqual(actual[prop], expected[prop], depth + 1, prop, index);
}
}
}
function nextAncestor(host: Node, node: Node): Node | null {
const parent = node.parentNode ?? (node as ShadowRoot).host ?? null;
if (parent === null || parent === host) {
return null;
}
return parent.nextSibling ?? nextAncestor(host, parent);
}
function nextNode(host: Node, node: Node): Node | null {
return CustomElement.for(node, { optional: true })?.shadowRoot?.firstChild ?? node.firstChild ?? node.nextSibling ?? nextAncestor(host, node);
}
export function getVisibleText(host: Node, removeWhiteSpace?: boolean): string | null {
let text = '';
let cur = CustomElement.for(host, { optional: true })?.shadowRoot?.firstChild ?? host.firstChild as Node | null;
while (cur !== null) {
if (cur.nodeType === NodeType.Text) {
text += (cur as Text).data;
}
cur = nextNode(host, cur);
}
return removeWhiteSpace && text ? text.replace(/\s\s+/g, ' ').trim() : text;
}
export function instructionTypeName(type: string): string {
switch (type) {
case InstructionType.textBinding:
return 'textBinding';
case InstructionType.interpolation:
return 'interpolation';
case InstructionType.propertyBinding:
return 'propertyBinding';
case InstructionType.iteratorBinding:
return 'iteratorBinding';
case InstructionType.listenerBinding:
return 'listenerBinding';
case InstructionType.refBinding:
return 'refBinding';
case InstructionType.stylePropertyBinding:
return 'stylePropertyBinding';
case InstructionType.setProperty:
return 'setProperty';
case InstructionType.setAttribute:
return 'setAttribute';
case InstructionType.hydrateElement:
return 'hydrateElement';
case InstructionType.hydrateAttribute:
return 'hydrateAttribute';
case InstructionType.hydrateTemplateController:
return 'hydrateTemplateController';
case InstructionType.hydrateLetElement:
return 'hydrateLetElement';
case InstructionType.letBinding:
return 'letBinding';
default:
return type;
}
}
export function verifyBindingInstructionsEqual(actual: any, expected: any, errors?: string[], path?: string): any {
if (path === undefined) {
path = 'instruction';
}
if (errors === undefined) {
errors = [];
}
if (!(expected instanceof Object) || !(actual instanceof Object)) {
if (actual?.nodeType > 0 && typeof expected === 'string' || typeof actual === 'string' && expected?.nodeType > 0) {
actual = typeof actual === 'string' ? actual : actual.outerHTML;
expected = typeof expected === 'string' ? expected : expected.outerHTML;
}
if (actual !== expected) {
// Special treatment for generated names (TODO: we *can* predict the values and we might want to at some point,
// because this exception is essentially a loophole that will eventually somehow cause a bug to slip through)
if (path.endsWith('.name')) {
if (String(expected) === 'unnamed' && String(actual).startsWith('unnamed-')) {
errors.push(`OK : ${path} === ${expected} (${actual})`);
}
} else if (path.endsWith('.key')) {
if (String(expected).endsWith('unnamed') && /unnamed-\d+$/.test(String(actual))) {
errors.push(`OK : ${path} === ${expected} (${actual})`);
}
} else {
if (typeof expected === 'object' && expected != null) {
expected = JSON.stringify(expected);
}
if (typeof actual === 'object' && actual != null) {
actual = JSON.stringify(actual);
}
if (path.endsWith('type')) {
expected = instructionTypeName(expected);
actual = instructionTypeName(actual);
}
errors.push(`WRONG: ${path} === ${actual} (expected: ${expected})`);
}
} else {
errors.push(`OK : ${path} === ${expected}`);
}
} else if (expected instanceof Array) {
for (let i = 0, ii = Math.max(expected.length, actual.length); i < ii; ++i) {
verifyBindingInstructionsEqual(actual[i], expected[i], errors, `${path}[${i}]`);
}
} else if (expected.nodeType > 0) {
if (expected.nodeType === 11) {
for (let i = 0, ii = Math.max(expected.childNodes.length, actual.childNodes.length); i < ii; ++i) {
verifyBindingInstructionsEqual(actual.childNodes.item(i), expected.childNodes.item(i), errors, `${path}.childNodes[${i}]`);
}
} else {
if (actual.outerHTML !== expected['outerHTML']) {
errors.push(`WRONG: ${path}.outerHTML === ${actual.outerHTML} (expected: ${expected['outerHTML']})`);
} else {
errors.push(`OK : ${path}.outerHTML === ${expected}`);
}
}
} else if (actual) {
const seen: Record<string, boolean> = {};
for (const prop in expected) {
verifyBindingInstructionsEqual(actual[prop], expected[prop], errors, `${path}.${prop}`);
seen[prop] = true;
}
for (const prop in actual) {
if (!seen[prop]) {
verifyBindingInstructionsEqual(actual[prop], expected[prop], errors, `${path}.${prop}`);
}
}
}
if (path === 'instruction' && errors.some(e => e.startsWith('W'))) {
throw new Error(`Failed assertion: binding instruction mismatch\n - ${errors.join('\n - ')}`);
}
}
_START_CONST_ENUM();
const enum NodeType {
Element = 1,
Attr = 2,
Text = 3,
CDATASection = 4,
EntityReference = 5,
Entity = 6,
ProcessingInstruction = 7,
Comment = 8,
Document = 9,
DocumentType = 10,
DocumentFragment = 11,
Notation = 12
}
_END_CONST_ENUM();