Last active
January 7, 2019 22:29
-
-
Save gund/709cd9628b0308ad724208ae843a9784 to your computer and use it in GitHub Desktop.
Angular Testing Helpers for auto-generating host component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { setOutputsMock } from './testing-helpers'; | |
declare module './testing-helpers' { | |
interface MockedOutput extends jest.Mock {} | |
} | |
setOutputsMock(() => jest.fn()); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { CommonModule } from '@angular/common'; | |
import { | |
Component, | |
ComponentFactory, | |
ComponentFactoryResolver, | |
DebugElement, | |
EventEmitter, | |
NgModule, | |
NO_ERRORS_SCHEMA, | |
Type, | |
ViewChild, | |
} from '@angular/core'; | |
import { ComponentFixture, TestBed, TestModuleMetadata } from '@angular/core/testing'; | |
import { By } from '@angular/platform-browser'; | |
export interface TestModuleExtras extends TestModuleMetadata { | |
projectContent?: string; | |
templateBindings?: string | {}; | |
} | |
export interface TestModuleDirExtras extends TestModuleExtras { | |
hostTag?: string; | |
hostComp?: Type<any>; | |
useStarSyntax?: boolean; | |
} | |
export interface AnyHostComponent<T> { | |
instance: T | undefined; | |
} | |
export type AsHostComponent<T, O = MockedOutput> = AnyHostComponent<T> & | |
{ [K in keyof T]: T[K] extends EventEmitter<any> ? O : T[K] }; | |
export interface DebugElementTyped<T> extends DebugElement { | |
readonly componentInstance: T; | |
} | |
export interface Host<T> { | |
hostComponentType: Type<AsHostComponent<T>>; | |
createHostComponent: () => ComponentFixture<AsHostComponent<T>>; | |
overrideHostTemplate: (tpl: string) => void; | |
getFixture: () => ComponentFixture<AsHostComponent<T>>; | |
detectChanges: ComponentFixture<any>['detectChanges']; | |
query: <C>(compOrDir: Type<C>) => DebugElementTyped<C> | null; | |
queryComponent: <C>(compOrDir: Type<C>) => C | undefined; | |
queryCss: (css: string) => DebugElement | null; | |
getHostElem: () => DebugElementTyped<AsHostComponent<T>>; | |
getHostComponent: () => AsHostComponent<T>; | |
} | |
export interface HostComponent<T> extends Host<T> { | |
getComponentElem: () => DebugElementTyped<T>; | |
getComponent: () => T; | |
} | |
export interface HostDirective<T> extends Host<T> { | |
getDirective: () => T; | |
} | |
export interface HostModule { | |
testingModule: Type<{}>; | |
} | |
export interface HostComponentModule<T> extends HostComponent<T>, HostModule {} | |
export interface HostDirectiveModule<T> extends HostDirective<T>, HostModule {} | |
export type TemplateBindings = ComponentFactory<any>['inputs']; | |
export interface DirectiveIO { | |
inputs: TemplateBindings; | |
outputs: TemplateBindings; | |
} | |
// tslint:disable-next-line:no-empty-interface | |
export interface MockedOutput {} | |
export type MockOutputFactory = ( | |
outputName: string, | |
compName: string, | |
getComp: () => any, | |
) => MockedOutput; | |
let mockOutputFactory: MockOutputFactory = () => (() => null) as any; | |
export function setOutputsMock(mock: MockOutputFactory) { | |
mockOutputFactory = mock; | |
} | |
export function configureTestingComponentWithHost<T>( | |
compType: Type<T>, | |
meta?: TestModuleExtras, | |
): HostComponentModule<T> { | |
return configureTestingWithHost(createHostComponentModule(compType, meta)); | |
} | |
export function configureTestingDirectiveWithHost<T>( | |
dirType: Type<T>, | |
meta?: TestModuleDirExtras, | |
): HostDirectiveModule<T> { | |
return configureTestingWithHost( | |
createHostDirectiveModule(dirType, meta, meta.hostTag || meta.hostComp), | |
); | |
} | |
export function configureTestingWithHost<T extends HostModule>(host: T): T { | |
TestBed.configureTestingModule({ imports: [host.testingModule] }); | |
return host; | |
} | |
export function createHostComponentModule<T>( | |
compType: Type<T>, | |
extraMeta?: TestModuleExtras, | |
): HostComponentModule<T> { | |
const host = createTestingComponentWithHost(compType, extraMeta); | |
return { | |
...host, | |
testingModule: createTestingModule(compType, host.hostComponentType, extraMeta), | |
}; | |
} | |
export function createHostDirectiveModule<T>( | |
dirType: Type<T>, | |
extraMeta?: TestModuleExtras, | |
tagOrComp?: string | Type<any>, | |
): HostDirectiveModule<T> { | |
const host = createTestingDirectiveWithHost(dirType, extraMeta, tagOrComp); | |
return { | |
...host, | |
testingModule: createTestingModule(dirType, host.hostComponentType, extraMeta), | |
}; | |
} | |
export function createTestingModule( | |
type: Type<any>, | |
hostType: Type<any>, | |
extraMeta?: TestModuleExtras, | |
) { | |
@NgModule({ | |
...extraMeta, | |
imports: [CommonModule, ...(extraMeta.imports || [])], | |
declarations: [type, hostType, ...(extraMeta.declarations || [])], | |
exports: [type, hostType], | |
}) | |
class TestModule {} | |
return TestModule; | |
} | |
export function createTestingComponentWithHost<T>( | |
compType: Type<T>, | |
extraMeta?: TestModuleExtras, | |
): HostComponent<T> { | |
const hostComponentType = generateHostComponentForComp(compType, extraMeta); | |
const host = createTestingHost(hostComponentType); | |
const getComponentElem = () => host.query(compType); | |
const getComponent = () => getComponentElem().componentInstance as T; | |
return { | |
...host, | |
getComponentElem, | |
getComponent, | |
}; | |
} | |
export function createTestingDirectiveWithHost<T>( | |
dirType: Type<T>, | |
extraMeta?: TestModuleExtras, | |
tagOrComp?: string | Type<any>, | |
): HostDirective<T> { | |
const hostComponentType = generateHostComponentForDir(dirType, extraMeta, tagOrComp); | |
const host = createTestingHost(hostComponentType); | |
const getDirective = () => host.getHostComponent().instance; | |
return { ...host, getDirective }; | |
} | |
export function createTestingHost<T>(hostComponentType: Type<AsHostComponent<T>>): Host<T> { | |
let _fixture: ComponentFixture<AsHostComponent<T>>; | |
const createHostComponent = () => TestBed.createComponent(hostComponentType); | |
const overrideHostTemplate = (tpl: string) => TestBed.overrideTemplate(hostComponentType, tpl); | |
const getFixture = () => (_fixture ? _fixture : (_fixture = createHostComponent())); | |
const detectChanges = (checkNoChanges?: boolean) => getFixture().detectChanges(checkNoChanges); | |
const query = (c: any) => getFixture().debugElement.query(By.directive(c)); | |
const queryComponent = (c: any) => query(c).componentInstance; | |
const queryCss = (css: string) => getFixture().debugElement.query(By.css(css)); | |
const getHostElem = () => getFixture().debugElement; | |
const getHostComponent = () => getFixture().componentInstance; | |
return { | |
hostComponentType, | |
createHostComponent, | |
overrideHostTemplate, | |
getFixture, | |
detectChanges, | |
query, | |
queryComponent, | |
queryCss, | |
getHostElem, | |
getHostComponent, | |
}; | |
} | |
export function generateHostComponentForComp<T>( | |
compType: Type<T>, | |
extraMeta?: TestModuleExtras, | |
): Type<AsHostComponent<T>> { | |
const compFactory = getCompFactory(compType, extraMeta); | |
const selector = `host-${compFactory.selector}`; | |
const template = generateHostCompTemplate( | |
compFactory.selector, | |
compFactory, | |
extraMeta.projectContent, | |
extraMeta.templateBindings, | |
); | |
return generateHostComponent({ selector, template }, compType, compFactory); | |
} | |
export function generateHostComponentForDir<T>( | |
dirType: Type<T>, | |
extraMeta?: TestModuleExtras, | |
tagOrComp?: string | Type<any>, | |
): Type<AsHostComponent<T>> { | |
const io = getDirectiveIO(dirType); | |
const selector = `host-dir`; | |
const templateTag = | |
typeof tagOrComp === 'function' ? getCompFactory(tagOrComp, extraMeta).selector : tagOrComp; | |
const template = tagOrComp | |
? generateHostCompTplForDir(io.inputs[0].templateName, templateTag, io, extraMeta) | |
: ``; | |
return generateHostComponent({ selector, template }, dirType, io); | |
} | |
export function generateHostComponent<T>( | |
meta: Component, | |
type: Type<T>, | |
io: DirectiveIO, | |
): Type<AsHostComponent<T>> { | |
@Component(meta) | |
class TestHostComponent implements AnyHostComponent<T> { | |
@ViewChild(type) | |
instance: T; | |
constructor() { | |
initHostComp(type, this, io); | |
} | |
} | |
return TestHostComponent as Type<AsHostComponent<T>>; | |
} | |
export function initHostComp( | |
compType: Type<any>, | |
hostComp: AnyHostComponent<any>, | |
io: DirectiveIO, | |
) { | |
io.inputs.forEach(({ propName }) => (hostComp[propName] = undefined)); | |
io.outputs.forEach( | |
({ propName }) => | |
(hostComp[propName] = mockOutputFactory(propName, compType.name, () => hostComp.instance)), | |
); | |
} | |
export function generateHostCompTplForDir( | |
binding: string, | |
tag: string, | |
io: DirectiveIO, | |
extra?: TestModuleDirExtras, | |
): string { | |
return extra.useStarSyntax | |
? generateHostCompTemplateStar(binding, tag, io, extra.projectContent, extra.templateBindings) | |
: generateHostCompTemplate(tag, io, extra.projectContent, extra.templateBindings); | |
} | |
export function generateHostCompTemplate( | |
tag: string, | |
io: DirectiveIO, | |
content: string = '', | |
bindings?: string | {}, | |
): string { | |
const inputsTpl = generateInputsTemplate(io.inputs); | |
const outputsTpl = generateOutputsTemplate(io.outputs); | |
const bindingsTpl = bindings | |
? typeof bindings === 'string' | |
? `let-${bindings}` | |
: Object.keys(bindings) | |
.map(key => `let-${key}${bindings[key] ? `="${bindings[key]}"` : ''}`) | |
.join(' ') | |
: ''; | |
return `<${tag} ${inputsTpl} ${outputsTpl} ${bindingsTpl}>${content}</${tag}>`; | |
} | |
export function generateHostCompTemplateStar( | |
binding: string, | |
tag: string, | |
io: DirectiveIO, | |
content: string = '', | |
bindings?: string | {}, | |
): string { | |
const inputsTpl = generateInputsTemplateStar(binding, io.inputs, bindings); | |
return `<${tag} ${inputsTpl}>${content}</${tag}>`; | |
} | |
export function generateInputsTemplateStar( | |
binding: string, | |
inputs: TemplateBindings, | |
bindings?: string | {}, | |
): string { | |
const inputsTpl = inputs | |
.filter(({ templateName }) => templateName !== binding) | |
.map(({ templateName, propName }) => `${templateName.replace(binding, '')}: ${propName}`) | |
.join('; '); | |
const bindingsTpl = bindings | |
? typeof bindings === 'string' | |
? `let ${bindings}` | |
: Object.keys(bindings) | |
.map(key => `let ${key}${bindings[key] ? `: ${bindings[key]}` : ''}`) | |
.join(', ') | |
: ''; | |
return `*${binding}="${binding}; ${inputsTpl}; ${bindingsTpl}"`; | |
} | |
export function generateInputsTemplate(inputs: TemplateBindings): string { | |
return inputs.map(({ templateName, propName }) => `[${templateName}]="${propName}"`).join(' '); | |
} | |
export function generateOutputsTemplate(outputs: TemplateBindings): string { | |
return outputs | |
.map(({ templateName, propName }) => `(${templateName})="${propName}($event)"`) | |
.join(' '); | |
} | |
export function getCompFactory<T>( | |
compType: Type<T>, | |
extraMeta: TestModuleExtras = {}, | |
): ComponentFactory<T> { | |
@NgModule({ | |
...extraMeta, | |
imports: [CommonModule, ...(extraMeta.imports || [])], | |
declarations: [compType, ...(extraMeta.declarations || [])], | |
exports: [compType], | |
entryComponents: [compType], | |
schemas: [NO_ERRORS_SCHEMA], | |
}) | |
class TestModule {} | |
TestBed.resetTestingModule().configureTestingModule({ imports: [TestModule] }); | |
const cfr = TestBed.get(ComponentFactoryResolver) as ComponentFactoryResolver; | |
const factory = cfr.resolveComponentFactory(compType); | |
TestBed.resetTestingModule(); | |
return factory; | |
} | |
export function getDirectiveIO<T>(dirType: Type<T>): DirectiveIO { | |
const { inputs, outputs } = (dirType as any).ngBaseDef; | |
const defToIO = def => | |
Object.keys(def).map(key => ({ propName: key, templateName: def[key] || key })); | |
return { | |
inputs: defToIO(inputs), | |
outputs: defToIO(outputs), | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Implemented here: https://github.com/orchestratora/ngx-testing