angular-cn/aio/src/app/custom-elements/elements-loader.spec.ts

297 lines
9.5 KiB
TypeScript
Raw Normal View History

import {
ComponentFactory,
ComponentFactoryResolver, ComponentRef, Injector, NgModuleFactory, NgModuleFactoryLoader,
NgModuleRef,
Type
} from '@angular/core';
import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing';
import { ElementsLoader } from './elements-loader';
import { ELEMENT_MODULE_PATHS_TOKEN, WithCustomElementComponent } from './element-registry';
interface Deferred {
resolve(): void;
reject(err: any): void;
}
describe('ElementsLoader', () => {
let elementsLoader: ElementsLoader;
beforeEach(() => {
const injector = TestBed.configureTestingModule({
providers: [
ElementsLoader,
{ provide: NgModuleFactoryLoader, useClass: FakeModuleFactoryLoader },
{ provide: ELEMENT_MODULE_PATHS_TOKEN, useValue: new Map([
['element-a-selector', 'element-a-module-path'],
['element-b-selector', 'element-b-module-path']
])},
]
});
elementsLoader = injector.get(ElementsLoader);
});
describe('loadContainedCustomElements()', () => {
let loadCustomElementSpy: jasmine.Spy;
beforeEach(() => loadCustomElementSpy = spyOn(elementsLoader, 'loadCustomElement'));
it('should attempt to load and register all contained elements', fakeAsync(() => {
expect(loadCustomElementSpy).not.toHaveBeenCalled();
const hostEl = document.createElement('div');
hostEl.innerHTML = `
<element-a-selector></element-a-selector>
<element-b-selector></element-b-selector>
`;
elementsLoader.loadContainedCustomElements(hostEl);
flushMicrotasks();
expect(loadCustomElementSpy).toHaveBeenCalledTimes(2);
expect(loadCustomElementSpy).toHaveBeenCalledWith('element-a-selector');
expect(loadCustomElementSpy).toHaveBeenCalledWith('element-b-selector');
}));
it('should attempt to load and register only contained elements', fakeAsync(() => {
expect(loadCustomElementSpy).not.toHaveBeenCalled();
const hostEl = document.createElement('div');
hostEl.innerHTML = `
<element-b-selector></element-b-selector>
`;
elementsLoader.loadContainedCustomElements(hostEl);
flushMicrotasks();
expect(loadCustomElementSpy).toHaveBeenCalledTimes(1);
expect(loadCustomElementSpy).toHaveBeenCalledWith('element-b-selector');
}));
it('should wait for all contained elements to load and register', fakeAsync(() => {
const deferreds = returnPromisesFromSpy(loadCustomElementSpy);
const hostEl = document.createElement('div');
hostEl.innerHTML = `
<element-a-selector></element-a-selector>
<element-b-selector></element-b-selector>
`;
const log: any[] = [];
elementsLoader.loadContainedCustomElements(hostEl).subscribe(
v => log.push(`emitted: ${v}`),
e => log.push(`errored: ${e}`),
() => log.push('completed'),
);
flushMicrotasks();
expect(log).toEqual([]);
deferreds[0].resolve();
flushMicrotasks();
expect(log).toEqual([]);
deferreds[1].resolve();
flushMicrotasks();
expect(log).toEqual(['emitted: undefined', 'completed']);
}));
it('should fail if any of the contained elements fails to load and register', fakeAsync(() => {
const deferreds = returnPromisesFromSpy(loadCustomElementSpy);
const hostEl = document.createElement('div');
hostEl.innerHTML = `
<element-a-selector></element-a-selector>
<element-b-selector></element-b-selector>
`;
const log: any[] = [];
elementsLoader.loadContainedCustomElements(hostEl).subscribe(
v => log.push(`emitted: ${v}`),
e => log.push(`errored: ${e}`),
() => log.push('completed'),
);
flushMicrotasks();
expect(log).toEqual([]);
deferreds[0].resolve();
flushMicrotasks();
expect(log).toEqual([]);
deferreds[1].reject('foo');
flushMicrotasks();
expect(log).toEqual(['errored: foo']);
}));
});
describe('loadCustomElement()', () => {
let definedSpy: jasmine.Spy;
let whenDefinedSpy: jasmine.Spy;
let whenDefinedDeferreds: Deferred[];
beforeEach(() => {
// `loadCustomElement()` uses the `window.customElements` API. Provide mocks for these tests.
definedSpy = spyOn(window.customElements, 'define');
whenDefinedSpy = spyOn(window.customElements, 'whenDefined');
whenDefinedDeferreds = returnPromisesFromSpy(whenDefinedSpy);
});
it('should be able to load and register an element', fakeAsync(() => {
elementsLoader.loadCustomElement('element-a-selector');
flushMicrotasks();
expect(definedSpy).toHaveBeenCalledTimes(1);
expect(definedSpy).toHaveBeenCalledWith('element-a-selector', jasmine.any(Function));
// Verify the right component was loaded/registered.
const Ctor = definedSpy.calls.argsFor(0)[1];
expect(Ctor.observedAttributes).toEqual(['element-a-module-path']);
}));
it('should wait until the element is defined', fakeAsync(() => {
let state = 'pending';
elementsLoader.loadCustomElement('element-b-selector').then(() => state = 'resolved');
flushMicrotasks();
expect(state).toBe('pending');
expect(whenDefinedSpy).toHaveBeenCalledTimes(1);
expect(whenDefinedSpy).toHaveBeenCalledWith('element-b-selector');
whenDefinedDeferreds[0].resolve();
flushMicrotasks();
expect(state).toBe('resolved');
}));
it('should not load and register the same element more than once', fakeAsync(() => {
elementsLoader.loadCustomElement('element-a-selector');
flushMicrotasks();
expect(definedSpy).toHaveBeenCalledTimes(1);
definedSpy.calls.reset();
// While loading/registering is still in progress:
elementsLoader.loadCustomElement('element-a-selector');
flushMicrotasks();
expect(definedSpy).not.toHaveBeenCalled();
definedSpy.calls.reset();
whenDefinedDeferreds[0].resolve();
// Once loading/registering is already completed:
let state = 'pending';
elementsLoader.loadCustomElement('element-a-selector').then(() => state = 'resolved');
flushMicrotasks();
expect(state).toBe('resolved');
expect(definedSpy).not.toHaveBeenCalled();
}));
it('should fail if defining the the custom element fails', fakeAsync(() => {
let state = 'pending';
elementsLoader.loadCustomElement('element-b-selector').catch(e => state = `rejected: ${e}`);
flushMicrotasks();
expect(state).toBe('pending');
whenDefinedDeferreds[0].reject('foo');
flushMicrotasks();
expect(state).toBe('rejected: foo');
}));
it('should be able to load and register an element again if previous attempt failed',
fakeAsync(() => {
elementsLoader.loadCustomElement('element-a-selector');
flushMicrotasks();
expect(definedSpy).toHaveBeenCalledTimes(1);
definedSpy.calls.reset();
// While loading/registering is still in progress:
elementsLoader.loadCustomElement('element-a-selector').catch(() => undefined);
flushMicrotasks();
expect(definedSpy).not.toHaveBeenCalled();
whenDefinedDeferreds[0].reject('foo');
flushMicrotasks();
expect(definedSpy).not.toHaveBeenCalled();
// Once loading/registering has already failed:
elementsLoader.loadCustomElement('element-a-selector');
flushMicrotasks();
expect(definedSpy).toHaveBeenCalledTimes(1);
})
);
});
});
// TEST CLASSES/HELPERS
class FakeCustomElementModule implements WithCustomElementComponent {
customElementComponent: Type<any>;
}
class FakeComponentFactory extends ComponentFactory<any> {
selector: string;
componentType: Type<any>;
ngContentSelectors: string[];
inputs = [{propName: this.identifyingInput, templateName: this.identifyingInput}];
outputs = [];
constructor(private identifyingInput: string) { super(); }
create(injector: Injector,
projectableNodes?: any[][],
rootSelectorOrNode?: string | any,
ngModule?: NgModuleRef<any>): ComponentRef<any> {
return jasmine.createSpy('ComponentRef') as any;
};
}
class FakeComponentFactoryResolver extends ComponentFactoryResolver {
constructor(private modulePath) { super(); }
resolveComponentFactory(component: Type<any>): ComponentFactory<any> {
return new FakeComponentFactory(this.modulePath);
}
}
class FakeModuleRef extends NgModuleRef<WithCustomElementComponent> {
injector = jasmine.createSpyObj('injector', ['get']);
componentFactoryResolver = new FakeComponentFactoryResolver(this.modulePath);
instance: WithCustomElementComponent = new FakeCustomElementModule();
constructor(private modulePath) {
super();
this.injector.get.and.returnValue(this.componentFactoryResolver);
}
destroy() {}
onDestroy(callback: () => void) {}
}
class FakeModuleFactory extends NgModuleFactory<any> {
moduleType: Type<any>;
moduleRefToCreate = new FakeModuleRef(this.modulePath);
constructor(private modulePath) { super(); }
create(parentInjector: Injector | null): NgModuleRef<any> {
return this.moduleRefToCreate;
}
}
class FakeModuleFactoryLoader extends NgModuleFactoryLoader {
load(modulePath: string): Promise<NgModuleFactory<any>> {
const fakeModuleFactory = new FakeModuleFactory(modulePath);
return Promise.resolve(fakeModuleFactory);
}
}
function returnPromisesFromSpy(spy: jasmine.Spy): Deferred[] {
const deferreds: Deferred[] = [];
spy.and.callFake(() => new Promise((resolve, reject) => deferreds.push({resolve, reject})));
return deferreds;
}