view-registry-spec.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /*
  2. * decaffeinate suggestions:
  3. * DS102: Remove unnecessary code created because of implicit returns
  4. * DS207: Consider shorter variations of null checks
  5. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  6. */
  7. const ViewRegistry = require('../src/view-registry');
  8. describe('ViewRegistry', () => {
  9. let registry = null;
  10. beforeEach(() => {
  11. registry = new ViewRegistry();
  12. });
  13. afterEach(() => {
  14. registry.clearDocumentRequests();
  15. });
  16. describe('::getView(object)', () => {
  17. describe('when passed a DOM node', () =>
  18. it('returns the given DOM node', () => {
  19. const node = document.createElement('div');
  20. expect(registry.getView(node)).toBe(node);
  21. }));
  22. describe('when passed an object with an element property', () =>
  23. it("returns the element property if it's an instance of HTMLElement", () => {
  24. class TestComponent {
  25. constructor() {
  26. this.element = document.createElement('div');
  27. }
  28. }
  29. const component = new TestComponent();
  30. expect(registry.getView(component)).toBe(component.element);
  31. }));
  32. describe('when passed an object with a getElement function', () =>
  33. it("returns the return value of getElement if it's an instance of HTMLElement", () => {
  34. class TestComponent {
  35. getElement() {
  36. if (this.myElement == null) {
  37. this.myElement = document.createElement('div');
  38. }
  39. return this.myElement;
  40. }
  41. }
  42. const component = new TestComponent();
  43. expect(registry.getView(component)).toBe(component.myElement);
  44. }));
  45. describe('when passed a model object', () => {
  46. describe("when a view provider is registered matching the object's constructor", () =>
  47. it('constructs a view element and assigns the model on it', () => {
  48. class TestModel {}
  49. class TestModelSubclass extends TestModel {}
  50. class TestView {
  51. initialize(model) {
  52. this.model = model;
  53. return this;
  54. }
  55. }
  56. const model = new TestModel();
  57. registry.addViewProvider(TestModel, model =>
  58. new TestView().initialize(model)
  59. );
  60. const view = registry.getView(model);
  61. expect(view instanceof TestView).toBe(true);
  62. expect(view.model).toBe(model);
  63. const subclassModel = new TestModelSubclass();
  64. const view2 = registry.getView(subclassModel);
  65. expect(view2 instanceof TestView).toBe(true);
  66. expect(view2.model).toBe(subclassModel);
  67. }));
  68. describe('when a view provider is registered generically, and works with the object', () =>
  69. it('constructs a view element and assigns the model on it', () => {
  70. registry.addViewProvider(model => {
  71. if (model.a === 'b') {
  72. const element = document.createElement('div');
  73. element.className = 'test-element';
  74. return element;
  75. }
  76. });
  77. const view = registry.getView({ a: 'b' });
  78. expect(view.className).toBe('test-element');
  79. expect(() => registry.getView({ a: 'c' })).toThrow();
  80. }));
  81. describe("when no view provider is registered for the object's constructor", () =>
  82. it('throws an exception', () => {
  83. expect(() => registry.getView({})).toThrow();
  84. }));
  85. });
  86. });
  87. describe('::addViewProvider(providerSpec)', () =>
  88. it('returns a disposable that can be used to remove the provider', () => {
  89. class TestModel {}
  90. class TestView {
  91. initialize(model) {
  92. this.model = model;
  93. return this;
  94. }
  95. }
  96. const disposable = registry.addViewProvider(TestModel, model =>
  97. new TestView().initialize(model)
  98. );
  99. expect(registry.getView(new TestModel()) instanceof TestView).toBe(true);
  100. disposable.dispose();
  101. expect(() => registry.getView(new TestModel())).toThrow();
  102. }));
  103. describe('::updateDocument(fn) and ::readDocument(fn)', () => {
  104. let frameRequests = null;
  105. beforeEach(() => {
  106. frameRequests = [];
  107. spyOn(window, 'requestAnimationFrame').andCallFake(fn =>
  108. frameRequests.push(fn)
  109. );
  110. });
  111. it('performs all pending writes before all pending reads on the next animation frame', () => {
  112. let events = [];
  113. registry.updateDocument(() => events.push('write 1'));
  114. registry.readDocument(() => events.push('read 1'));
  115. registry.readDocument(() => events.push('read 2'));
  116. registry.updateDocument(() => events.push('write 2'));
  117. expect(events).toEqual([]);
  118. expect(frameRequests.length).toBe(1);
  119. frameRequests[0]();
  120. expect(events).toEqual(['write 1', 'write 2', 'read 1', 'read 2']);
  121. frameRequests = [];
  122. events = [];
  123. const disposable = registry.updateDocument(() => events.push('write 3'));
  124. registry.updateDocument(() => events.push('write 4'));
  125. registry.readDocument(() => events.push('read 3'));
  126. disposable.dispose();
  127. expect(frameRequests.length).toBe(1);
  128. frameRequests[0]();
  129. expect(events).toEqual(['write 4', 'read 3']);
  130. });
  131. it('performs writes requested from read callbacks in the same animation frame', () => {
  132. spyOn(window, 'setInterval').andCallFake(fakeSetInterval);
  133. spyOn(window, 'clearInterval').andCallFake(fakeClearInterval);
  134. const events = [];
  135. registry.updateDocument(() => events.push('write 1'));
  136. registry.readDocument(() => {
  137. registry.updateDocument(() => events.push('write from read 1'));
  138. events.push('read 1');
  139. });
  140. registry.readDocument(() => {
  141. registry.updateDocument(() => events.push('write from read 2'));
  142. events.push('read 2');
  143. });
  144. registry.updateDocument(() => events.push('write 2'));
  145. expect(frameRequests.length).toBe(1);
  146. frameRequests[0]();
  147. expect(frameRequests.length).toBe(1);
  148. expect(events).toEqual([
  149. 'write 1',
  150. 'write 2',
  151. 'read 1',
  152. 'read 2',
  153. 'write from read 1',
  154. 'write from read 2'
  155. ]);
  156. });
  157. });
  158. describe('::getNextUpdatePromise()', () =>
  159. it('returns a promise that resolves at the end of the next update cycle', () => {
  160. let updateCalled = false;
  161. let readCalled = false;
  162. waitsFor('getNextUpdatePromise to resolve', done => {
  163. registry.getNextUpdatePromise().then(() => {
  164. expect(updateCalled).toBe(true);
  165. expect(readCalled).toBe(true);
  166. done();
  167. });
  168. registry.updateDocument(() => {
  169. updateCalled = true;
  170. });
  171. registry.readDocument(() => {
  172. readCalled = true;
  173. });
  174. });
  175. }));
  176. });