Skip to content

Instantly share code, notes, and snippets.

@gund
Last active January 7, 2019 22:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gund/709cd9628b0308ad724208ae843a9784 to your computer and use it in GitHub Desktop.
Save gund/709cd9628b0308ad724208ae843a9784 to your computer and use it in GitHub Desktop.
Angular Testing Helpers for auto-generating host component
import { setOutputsMock } from './testing-helpers';
declare module './testing-helpers' {
interface MockedOutput extends jest.Mock {}
}
setOutputsMock(() => jest.fn());
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),
};
}
@gund
Copy link
Author

gund commented Jan 7, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment