/* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const ViewRegistry = require('../src/view-registry'); describe('ViewRegistry', () => { let registry = null; beforeEach(() => { registry = new ViewRegistry(); }); afterEach(() => { registry.clearDocumentRequests(); }); describe('::getView(object)', () => { describe('when passed a DOM node', () => it('returns the given DOM node', () => { const node = document.createElement('div'); expect(registry.getView(node)).toBe(node); })); describe('when passed an object with an element property', () => it("returns the element property if it's an instance of HTMLElement", () => { class TestComponent { constructor() { this.element = document.createElement('div'); } } const component = new TestComponent(); expect(registry.getView(component)).toBe(component.element); })); describe('when passed an object with a getElement function', () => it("returns the return value of getElement if it's an instance of HTMLElement", () => { class TestComponent { getElement() { if (this.myElement == null) { this.myElement = document.createElement('div'); } return this.myElement; } } const component = new TestComponent(); expect(registry.getView(component)).toBe(component.myElement); })); describe('when passed a model object', () => { describe("when a view provider is registered matching the object's constructor", () => it('constructs a view element and assigns the model on it', () => { class TestModel {} class TestModelSubclass extends TestModel {} class TestView { initialize(model) { this.model = model; return this; } } const model = new TestModel(); registry.addViewProvider(TestModel, model => new TestView().initialize(model) ); const view = registry.getView(model); expect(view instanceof TestView).toBe(true); expect(view.model).toBe(model); const subclassModel = new TestModelSubclass(); const view2 = registry.getView(subclassModel); expect(view2 instanceof TestView).toBe(true); expect(view2.model).toBe(subclassModel); })); describe('when a view provider is registered generically, and works with the object', () => it('constructs a view element and assigns the model on it', () => { registry.addViewProvider(model => { if (model.a === 'b') { const element = document.createElement('div'); element.className = 'test-element'; return element; } }); const view = registry.getView({ a: 'b' }); expect(view.className).toBe('test-element'); expect(() => registry.getView({ a: 'c' })).toThrow(); })); describe("when no view provider is registered for the object's constructor", () => it('throws an exception', () => { expect(() => registry.getView({})).toThrow(); })); }); }); describe('::addViewProvider(providerSpec)', () => it('returns a disposable that can be used to remove the provider', () => { class TestModel {} class TestView { initialize(model) { this.model = model; return this; } } const disposable = registry.addViewProvider(TestModel, model => new TestView().initialize(model) ); expect(registry.getView(new TestModel()) instanceof TestView).toBe(true); disposable.dispose(); expect(() => registry.getView(new TestModel())).toThrow(); })); describe('::updateDocument(fn) and ::readDocument(fn)', () => { let frameRequests = null; beforeEach(() => { frameRequests = []; spyOn(window, 'requestAnimationFrame').andCallFake(fn => frameRequests.push(fn) ); }); it('performs all pending writes before all pending reads on the next animation frame', () => { let events = []; registry.updateDocument(() => events.push('write 1')); registry.readDocument(() => events.push('read 1')); registry.readDocument(() => events.push('read 2')); registry.updateDocument(() => events.push('write 2')); expect(events).toEqual([]); expect(frameRequests.length).toBe(1); frameRequests[0](); expect(events).toEqual(['write 1', 'write 2', 'read 1', 'read 2']); frameRequests = []; events = []; const disposable = registry.updateDocument(() => events.push('write 3')); registry.updateDocument(() => events.push('write 4')); registry.readDocument(() => events.push('read 3')); disposable.dispose(); expect(frameRequests.length).toBe(1); frameRequests[0](); expect(events).toEqual(['write 4', 'read 3']); }); it('performs writes requested from read callbacks in the same animation frame', () => { spyOn(window, 'setInterval').andCallFake(fakeSetInterval); spyOn(window, 'clearInterval').andCallFake(fakeClearInterval); const events = []; registry.updateDocument(() => events.push('write 1')); registry.readDocument(() => { registry.updateDocument(() => events.push('write from read 1')); events.push('read 1'); }); registry.readDocument(() => { registry.updateDocument(() => events.push('write from read 2')); events.push('read 2'); }); registry.updateDocument(() => events.push('write 2')); expect(frameRequests.length).toBe(1); frameRequests[0](); expect(frameRequests.length).toBe(1); expect(events).toEqual([ 'write 1', 'write 2', 'read 1', 'read 2', 'write from read 1', 'write from read 2' ]); }); }); describe('::getNextUpdatePromise()', () => it('returns a promise that resolves at the end of the next update cycle', () => { let updateCalled = false; let readCalled = false; waitsFor('getNextUpdatePromise to resolve', done => { registry.getNextUpdatePromise().then(() => { expect(updateCalled).toBe(true); expect(readCalled).toBe(true); done(); }); registry.updateDocument(() => { updateCalled = true; }); registry.readDocument(() => { readCalled = true; }); }); })); });