|
| 1 | +import type { Type, ComponentRef, ɵCssSelectorList, DebugNode } from '@angular/core'; |
| 2 | +import type { ComponentFixture } from '@angular/core/testing'; |
| 3 | +import type { Colors } from 'pretty-format'; |
| 4 | + |
| 5 | +/** |
| 6 | + * The follow interfaces are customized heavily inspired by @angular/core/core.d.ts |
| 7 | + */ |
| 8 | +interface ComponentDef { |
| 9 | + selectors: ɵCssSelectorList; |
| 10 | +} |
| 11 | +interface VEComponentType extends Type<unknown> { |
| 12 | + ngComponentDef: ComponentDef; |
| 13 | +} |
| 14 | +interface IvyComponentType extends Type<unknown> { |
| 15 | + ɵcmp: ComponentDef; |
| 16 | +} |
| 17 | +interface NgComponentRef extends ComponentRef<unknown> { |
| 18 | + _elDef: any; // eslint-disable-line @typescript-eslint/no-explicit-any |
| 19 | + _view: any; // eslint-disable-line @typescript-eslint/no-explicit-any |
| 20 | +} |
| 21 | +interface NgComponentFixture extends ComponentFixture<unknown> { |
| 22 | + componentRef: NgComponentRef; |
| 23 | + componentInstance: Record<string, unknown>; |
| 24 | +} |
| 25 | +interface VEDebugNode { |
| 26 | + renderElement: { |
| 27 | + childNodes: DebugNode[]; |
| 28 | + }; |
| 29 | +} |
| 30 | + |
| 31 | +/** |
| 32 | + * The following types haven't been exported by jest so temporarily we copy typings from 'pretty-format' |
| 33 | + */ |
| 34 | +interface PluginOptions { |
| 35 | + edgeSpacing: string; |
| 36 | + min: boolean; |
| 37 | + spacing: string; |
| 38 | +} |
| 39 | +type Indent = (indentSpaces: string) => string; |
| 40 | +type Printer = (elementToSerialize: unknown) => string; |
| 41 | + |
| 42 | +const printAttributes = ( |
| 43 | + val: any, // eslint-disable-line @typescript-eslint/no-explicit-any |
| 44 | + attributes: string[], |
| 45 | + _print: Printer, |
| 46 | + indent: Indent, |
| 47 | + colors: Colors, |
| 48 | + opts: PluginOptions, |
| 49 | +): string => { |
| 50 | + return attributes |
| 51 | + .sort() |
| 52 | + .map((attribute) => { |
| 53 | + return ( |
| 54 | + opts.spacing + |
| 55 | + indent(`${colors.prop.open}${attribute}${colors.prop.close}=`) + |
| 56 | + colors.value.open + |
| 57 | + (val.componentInstance[attribute] && val.componentInstance[attribute].constructor |
| 58 | + ? `{[Function ${val.componentInstance[attribute].constructor.name}]}` |
| 59 | + : `"${val.componentInstance[attribute]}"`) + |
| 60 | + colors.value.close |
| 61 | + ); |
| 62 | + }) |
| 63 | + .join(''); |
| 64 | +}; |
| 65 | + |
| 66 | +const ivyEnabled = (): boolean => { |
| 67 | + // Should be required lazily, since it will throw an exception |
| 68 | + // `Cannot resolve parameters...`. |
| 69 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires |
| 70 | + const { ɵivyEnabled } = require('@angular/core'); |
| 71 | + |
| 72 | + return !!ɵivyEnabled; |
| 73 | +}; |
| 74 | + |
| 75 | +// Ivy component definition was stored on the `ngComponentDef` |
| 76 | +// property before `9.0.0-next.10`. Since `9.0.0-next.10` it was |
| 77 | +// renamed to `ɵcmp`. |
| 78 | +const getComponentDef = (type: VEComponentType | IvyComponentType): ComponentDef => |
| 79 | + (type as VEComponentType).ngComponentDef ?? (type as IvyComponentType).ɵcmp; |
| 80 | + |
| 81 | +const print = ( |
| 82 | + fixture: NgComponentFixture, |
| 83 | + print: Printer, |
| 84 | + indent: Indent, |
| 85 | + opts: PluginOptions, |
| 86 | + colors: Colors, |
| 87 | +): string => { |
| 88 | + let nodes = ''; |
| 89 | + let componentAttrs = ''; |
| 90 | + let componentName = ''; |
| 91 | + |
| 92 | + if (ivyEnabled()) { |
| 93 | + const componentDef = getComponentDef(fixture.componentRef.componentType as IvyComponentType); |
| 94 | + componentName = componentDef.selectors[0][0] as string; |
| 95 | + nodes = Array.from(fixture.componentRef.location.nativeElement.childNodes).map(print).join(''); |
| 96 | + } else { |
| 97 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment |
| 98 | + componentName = fixture.componentRef._elDef.element?.name; |
| 99 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment |
| 100 | + nodes = (fixture.componentRef._view.nodes || []) |
| 101 | + // eslint-disable-next-line no-prototype-builtins |
| 102 | + .filter((node: VEDebugNode) => node?.hasOwnProperty('renderElement')) |
| 103 | + .map((node: VEDebugNode) => Array.from(node.renderElement.childNodes).map(print).join('')) |
| 104 | + .join(opts.edgeSpacing); |
| 105 | + } |
| 106 | + const attributes = Object.keys(fixture.componentInstance); |
| 107 | + if (attributes.length) { |
| 108 | + componentAttrs += printAttributes(fixture, attributes, print, indent, colors, opts); |
| 109 | + } |
| 110 | + |
| 111 | + return ( |
| 112 | + '<' + |
| 113 | + componentName + |
| 114 | + componentAttrs + |
| 115 | + (componentAttrs.length ? '\n' : '') + |
| 116 | + '>\n' + |
| 117 | + indent(nodes) + |
| 118 | + '\n</' + |
| 119 | + componentName + |
| 120 | + '>' |
| 121 | + ); |
| 122 | +}; |
| 123 | + |
| 124 | +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types |
| 125 | +const test = (val: any): boolean => |
| 126 | + val !== undefined && |
| 127 | + val !== null && |
| 128 | + typeof val === 'object' && |
| 129 | + Object.prototype.hasOwnProperty.call(val, 'componentRef'); |
| 130 | + |
| 131 | +export = { |
| 132 | + print, |
| 133 | + test, |
| 134 | +}; |
0 commit comments