UNPKG

opentok-layout-js

Version:

Automatic layout of video elements (publisher and subscriber) minimising white-space for the OpenTok on WebRTC API. This is intended for use with the OpenTok on WebRTC JS API.

1,278 lines (1,192 loc) 48.8 kB
/* globals describe, beforeEach, expect, it, afterEach, initLayoutContainer */ describe('opentok layout', () => { it('defines initLayoutContainer', () => { expect(typeof window.initLayoutContainer).toEqual('function'); }); it('defines layout, getLayout and setOptions methods', () => { const layoutDiv = document.createElement('div'); document.body.appendChild(layoutDiv); const layoutContainer = initLayoutContainer(layoutDiv); expect(typeof layoutContainer.layout).toEqual('function'); expect(typeof layoutContainer.getLayout).toEqual('function'); expect(typeof layoutContainer.setOptions).toEqual('function'); }); it('does not break jQuery', () => { expect(window.$).toBe(window.jQuery); }); describe('handling layout of 2 elements', () => { let layoutDiv; let div1; let div2; let expectedLayout; let children; beforeEach(() => { layoutDiv = document.createElement('div'); layoutDiv.setAttribute('id', 'layoutDiv'); layoutDiv.style.position = 'absolute'; layoutDiv.style.top = '0px'; layoutDiv.style.left = '0px'; layoutDiv.style.width = '400px'; layoutDiv.style.height = '300px'; layoutDiv.style.backgroundColor = 'grey'; document.body.style.margin = '0px'; document.body.style.padding = '0px'; document.body.appendChild(layoutDiv); div1 = document.createElement('div'); div2 = document.createElement('div'); div1.style.backgroundColor = 'green'; div2.style.backgroundColor = 'red'; layoutDiv.appendChild(div1); layoutDiv.appendChild(div2); children = [div1, div2]; expectedLayout = [{ width: 200, height: 300, left: 0, top: 0, }, { width: 200, height: 300, left: 200, top: 0, }]; }); afterEach(() => { document.body.removeChild(layoutDiv); layoutDiv = null; div1 = null; div2 = null; }); it('handles default getLayout', () => { const layoutContainer = initLayoutContainer({ containerWidth: 400, containerHeight: 300, }); expect(layoutContainer.getLayout(children.map(() => ({ width: 300, height: 200 })))) .toEqual({ boxes: expectedLayout, areas: { small: { top: 0, left: 0, width: 400, height: 300, }, }, }); }); it('triggers the onLayout method', () => { const layoutContainer = initLayoutContainer(layoutDiv, { onLayout: (elem, position) => { ['width', 'height', 'top', 'left'].forEach((key) => { expect( position[key] === expectedLayout[0][key] || position[key] === expectedLayout[1][key] ).toBe(true); }); }, }); layoutContainer.layout(); }); it('handles default layout', () => { const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); expectedLayout.forEach((box, idx) => { const boundingRect = children[idx].getBoundingClientRect(); ['width', 'height', 'left', 'top'].forEach((val) => { expect(boundingRect[val]).toBeCloseTo(expectedLayout[idx][val], 1); }); }); }); it('adds the "ot-layout" class to elements', () => { const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); expect(div1.classList.contains('ot-layout')).toBe(true); expect(div2.classList.contains('ot-layout')).toBe(true); }); it('maintains multiple aspect ratios if you set fixedRatio:true', () => { div1.videoWidth = 640; div1.videoHeight = 480; div2.videoWidth = 480; div2.videoHeight = 640; const layoutContainer = initLayoutContainer(layoutDiv, { fixedRatio: true }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); expect(div1Rect.width / div1Rect.height).toBeCloseTo(640 / 480, 1); const div2Rect = div2.getBoundingClientRect(); expect(div2Rect.width / div2Rect.height).toBeCloseTo(480 / 640, 1); }); it('grows to takes up the whole height if there are narrow elements', () => { // The second element is portrait and so it doesn't take up as much // space as the first one. So we should account for that and make the height larger div1.videoWidth = 640; div1.videoHeight = 480; div2.videoWidth = 480; div2.videoHeight = 640; layoutDiv.style.width = '660px'; const layoutContainer = initLayoutContainer(layoutDiv, { fixedRatio: true }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); const div2Rect = div2.getBoundingClientRect(); expect(div1Rect.height).toBeCloseTo(300, 1); expect(div2Rect.height).toBeCloseTo(300, 1); expect(div2Rect.width).not.toBeGreaterThan(640); }); it('adjusts to not go over the width if you have a wider element', () => { // The second element is 720p and so we need to adjust to not go over the width div1.videoWidth = 640; div1.videoHeight = 480; div2.videoWidth = 1280; div2.videoHeight = 720; layoutDiv.style.width = '600px'; const layoutContainer = initLayoutContainer(layoutDiv, { fixedRatio: true }); layoutContainer.layout(); const div2Rect = div2.getBoundingClientRect(); expect(div2Rect.left + div2Rect.width).toBeLessThan(600); }); it('lets you change the min and maxRatio to force a ratio', () => { const layoutContainer = initLayoutContainer(layoutDiv, { minRatio: 9 / 16, maxRatio: 9 / 16 }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); expect(div1Rect.width / div1Rect.height).toBeCloseTo(16 / 9, 3); }); describe('setOptions', () => { it('lets you update min and maxRatio options', () => { const layoutContainer = initLayoutContainer(layoutDiv, { minRatio: 9 / 16, maxRatio: 9 / 16 }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); expect(div1Rect.width / div1Rect.height).toBeCloseTo(16 / 9, 3); layoutContainer.setOptions({ minRatio: 3 / 4, maxRatio: 3 / 4 }); layoutContainer.layout(); const updatedDiv1Rect = div1.getBoundingClientRect(); expect(updatedDiv1Rect.width / updatedDiv1Rect.height).toBeCloseTo(4 / 3, 3); }); }); describe('alignItems', () => { ['start', 'center', 'end'].forEach((alignItems) => { it(`Handles ${alignItems}`, () => { const layoutContainer = initLayoutContainer(layoutDiv, { alignItems, minRatio: 10 / 16, maxRatio: 10 / 16, }); layoutContainer.layout(); const boundingRect = div1.getBoundingClientRect(); switch (alignItems) { case 'start': expect(boundingRect.left).toEqual(0); break; case 'end': expect(boundingRect.left).toBeCloseTo(160, 1); break; case 'center': default: expect(boundingRect.left).toBeCloseTo(80, 1); break; } }); }); }); describe('maxWidth and maxHeight', () => { it('does not go over the maxWidth and maxHeight and stays centered', () => { const layoutContainer = initLayoutContainer(layoutDiv, { maxWidth: 160, maxHeight: 100, }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); expect(div1Rect.width).toBeCloseTo(160, 1); expect(div1Rect.height).toBeCloseTo(100, 1); expect(div1Rect.left).toBeCloseTo(40, 1); expect(div1Rect.top).toBeCloseTo(100, 1); const div2Rect = div2.getBoundingClientRect(); expect(div2Rect.width).toBeCloseTo(160, 1); expect(div2Rect.height).toBeCloseTo(100, 1); expect(div2Rect.left).toBeCloseTo(200, 1); expect(div2Rect.top).toBeCloseTo(100, 1); }); }); describe('ignoreClass', () => { it('does not position items that are ignored', () => { div1.className = 'OT_ignore'; div1.style.position = 'absolute'; div1.style.top = '0px'; div1.style.left = '0px'; div1.style.width = '100px'; div1.style.height = '100px'; div1.style.zIndex = 1; const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); expect(div1Rect.width).toBeCloseTo(100, 1); expect(div1Rect.height).toBeCloseTo(100, 1); expect(div1Rect.top).toBeCloseTo(0, 1); expect(div1Rect.left).toBeCloseTo(0, 1); const div2Rect = div2.getBoundingClientRect(); expect(div2Rect.width).toBeCloseTo(400, 1); expect(div2Rect.height).toBeCloseTo(300, 1); expect(div2Rect.top).toBeCloseTo(0, 1); expect(div2Rect.left).toBeCloseTo(0, 1); }); }); describe('with a big element', () => { beforeEach(() => { div1.className = 'OT_big'; expectedLayout = [{ width: 320, height: 300, left: 0, top: 0, }, { width: 80, height: 120, left: 320, top: 90, }, ]; }); it('handles default getLayout', () => { const layoutContainer = initLayoutContainer({ containerWidth: 400, containerHeight: 300, }); expect(layoutContainer.getLayout(children.map(child => ({ width: 640, height: 480, big: child.className === 'OT_big', })))).toEqual({ boxes: expectedLayout, areas: { big: { top: 0, left: 0, width: 320, height: 300, }, small: { top: 0, left: 320, width: 80, height: 300, }, }, }); }); it('lets you update container size for getLayout', () => { const layoutContainer = initLayoutContainer({ containerWidth: 400, containerHeight: 300, }); expect(layoutContainer.getLayout(children.map(child => ({ width: 640, height: 480, big: child.className === 'OT_big', })))).toEqual({ boxes: expectedLayout, areas: { big: { top: 0, left: 0, width: 320, height: 300, }, small: { top: 0, left: 320, width: 80, height: 300, }, }, }); layoutContainer.setOptions({ containerWidth: 800, containerHeight: 600, }); expect(layoutContainer.getLayout(children.map(child => ({ width: 640, height: 480, big: child.className === 'OT_big', })))).toEqual({ boxes: [{ width: 640, height: 600, left: 0, top: 0, }, { width: 160, height: 240, left: 640, top: 180, }, ], areas: { big: { top: 0, left: 0, width: 640, height: 600, }, small: { top: 0, left: 640, width: 160, height: 600, }, }, }); }); it('handles default layout', () => { const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); expectedLayout.forEach((box, idx) => { const boundingRect = children[idx].getBoundingClientRect(); ['width', 'height', 'left', 'top'].forEach((val) => { expect(boundingRect[val]).toBeCloseTo(expectedLayout[idx][val], 1); }); }); }); it('handles bigFixedRatio:true', () => { const layoutContainer = initLayoutContainer(layoutDiv, { bigFixedRatio: true }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); expect(div1Rect.width / div1Rect.height).toBeCloseTo(4 / 3, 3); }); it('lets you change the bigMinRatio and bigMaxRatio to force a ratio', () => { const layoutContainer = initLayoutContainer(layoutDiv, { bigMinRatio: 9 / 16, bigMaxRatio: 9 / 16 }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); expect(div1Rect.width / div1Rect.height).toBeCloseTo(16 / 9, 3); }); it('handles bigPercentage', () => { const layoutContainer = initLayoutContainer(layoutDiv, { bigPercentage: 0.9 }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); const div2Rect = div2.getBoundingClientRect(); expect(div1Rect.width).toBeCloseTo(360, 1); expect(div2Rect.width).toBeCloseTo(40, 1); expect(div1Rect.height).toBeCloseTo(300, 1); expect(div2Rect.height).toBeCloseTo(60, 1); }); it('handles minBigPercentage', () => { layoutDiv.style.width = '800px'; let layoutContainer = initLayoutContainer(layoutDiv, { bigPercentage: 0.9, minBigPercentage: 0.8, }); layoutContainer.layout(); let div1Rect = div1.getBoundingClientRect(); let div2Rect = div2.getBoundingClientRect(); expect(div1Rect.width).toBeCloseTo(533, -1); expect(div2Rect.width).toBeCloseTo(160, 1); expect(div1Rect.height).toBeCloseTo(300, 1); expect(div2Rect.height).toBeCloseTo(240, 1); layoutContainer = initLayoutContainer(layoutDiv, { bigPercentage: 0.9, minBigPercentage: 0.6, }); layoutContainer.layout(); div1Rect = div1.getBoundingClientRect(); div2Rect = div2.getBoundingClientRect(); expect(div1Rect.width).toBeCloseTo(533, -1); expect(div2Rect.width).toBeCloseTo(266, 1); expect(div1Rect.height).toBeCloseTo(300, 1); expect(div2Rect.height).toBeCloseTo(300, 1); }); it('does not scale the big percentage down too small and leave floating elements', () => { // If you set the width to be wide layoutDiv.style.width = '1000px'; const layoutContainer = initLayoutContainer(layoutDiv, { bigPercentage: 0.9, minBigPercentage: 0.6, smallMaxWidth: 300, smallMaxHeight: 300, }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); const div2Rect = div2.getBoundingClientRect(); // Make sure div2 is right up against the right hand side expect(div2Rect.width + div2Rect.left).toBeCloseTo(1000, 1); expect(div1Rect.width).toBeCloseTo(533, -1); expect(div2Rect.width).toBeCloseTo(300, 1); expect(div1Rect.height).toBeCloseTo(300, 1); expect(div2Rect.height).toBeCloseTo(300, 1); expect(div2Rect.left).toBeCloseTo(700, 1); }); it('with fixedRatio does not scale the big percentage down too small and leave floating elements', () => { // If you set the width to be wide and have a portrait aspect ratio layoutDiv.style.width = '1000px'; div2.videoWidth = 360; div2.videoHeight = 480; const layoutContainer = initLayoutContainer(layoutDiv, { bigPercentage: 0.9, minBigPercentage: 0.6, smallMaxWidth: 300, smallMaxHeight: 300, fixedRatio: true, }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); const div2Rect = div2.getBoundingClientRect(); // Make sure div2 is right up against the right hand side, not floating expect(div2Rect.width + div2Rect.left).toBeCloseTo(1000, 1); expect(div1Rect.width).toBeCloseTo(533, -1); expect(div1Rect.height).toBeCloseTo(300, 1); expect(div2Rect.width).toBeCloseTo(225, 1); expect(div2Rect.height).toBeCloseTo(300, 1); }); it('when tall with fixedRatio does not scale the big percentage down too small and leave floating elements', () => { // If you set the width to be wide and have a landscape aspect ratio layoutDiv.style.height = '1000px'; div2.videoWidth = 1280; div2.videoHeight = 720; const layoutContainer = initLayoutContainer(layoutDiv, { bigPercentage: 0.9, minBigPercentage: 0.6, smallMaxWidth: 300, smallMaxHeight: 300, fixedRatio: true, }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); const div2Rect = div2.getBoundingClientRect(); // Make sure div2 is right up against the bottom, not floating expect(div2Rect.height + div2Rect.top).toBeCloseTo(1000, 1); expect(div1Rect.width).toBeCloseTo(400, -1); expect(div1Rect.height).toBeCloseTo(600, 1); expect(div2Rect.width).toBeCloseTo(400, 1); expect(div2Rect.height).toBeCloseTo(225, 1); }); it('handles bigFirst false', () => { const layoutContainer = initLayoutContainer(layoutDiv, { bigFirst: false }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); expect(div1Rect.left).toBeCloseTo(80, 1); expect(div1Rect.top).toBeCloseTo(0, 1); }); it('takes margin into account', () => { div1.style.margin = '5px'; div2.style.margin = '5px'; const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); const div2Rect = div2.getBoundingClientRect(); expect(div1Rect.width).toBeCloseTo(310, 1); expect(div2Rect.width).toBeCloseTo(70, 1); expect(div1Rect.height).toBeCloseTo(290, 1); expect(div2Rect.height).toBeCloseTo(110, 1); }); it('takes margin CSS styles into account', () => { const style = document.createElement('style'); style.innerHTML = '.margin { margin: 5px; }'; document.head.appendChild(style); div1.classList.add('margin'); div2.classList.add('margin'); const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); const div2Rect = div2.getBoundingClientRect(); expect(div1Rect.width).toBeCloseTo(310, 1); expect(div2Rect.width).toBeCloseTo(70, 1); expect(div1Rect.height).toBeCloseTo(290, 1); expect(div2Rect.height).toBeCloseTo(110, 1); }); it('takes padding into account', () => { div1.style.padding = '5px'; div2.style.padding = '5px'; const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); expect(div1.clientWidth).toBeCloseTo(320, 1); expect(div2.clientWidth).toBeCloseTo(80, -1); expect(div1.clientHeight).toBeCloseTo(300, 1); expect(div2.clientHeight).toBeCloseTo(120, 1); }); it('takes border into account', () => { const style = document.createElement('style'); style.innerHTML = '.border { border: 1px solid blue; }'; document.head.appendChild(style); div1.classList.add('border'); div2.classList.add('border'); const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); expect(div1.clientWidth).toBeCloseTo(318, 1); expect(div2.clientWidth).toBeCloseTo(78, -1); expect(div1.clientHeight).toBeCloseTo(298, 1); expect(div2.clientHeight).toBeCloseTo(118, 1); }); it('takes box-sizing into account', () => { const style = document.createElement('style'); style.innerHTML = '.box-sizing { padding: 5px; box-sizing: border-box; }'; document.head.appendChild(style); div1.classList.add('box-sizing'); div2.classList.add('box-sizing'); const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); expect(div1.clientWidth).toBeCloseTo(320, 1); expect(div2.clientWidth).toBeCloseTo(80, -1); expect(div1.clientHeight).toBeCloseTo(300, 1); expect(div2.clientHeight).toBeCloseTo(120, 1); }); describe('smallAlignItems', () => { ['start', 'center', 'end'].forEach((smallAlignItems) => { it(`Handles ${smallAlignItems}`, () => { const layoutContainer = initLayoutContainer(layoutDiv, { smallAlignItems }); layoutContainer.layout(); const boundingRect = children[1].getBoundingClientRect(); switch (smallAlignItems) { case 'start': expect(boundingRect.top).toBeCloseTo(0, 1); break; case 'end': expect(boundingRect.top).toBeCloseTo(180, 1); break; case 'center': default: expect(boundingRect.top).toBeCloseTo(90, 1); break; } }); }); }); describe('bigAlignItems', () => { ['start', 'center', 'end'].forEach((bigAlignItems) => { it(`Handles ${bigAlignItems}`, () => { const layoutContainer = initLayoutContainer(layoutDiv, { bigAlignItems, bigFixedRatio: true, }); layoutContainer.layout(); const boundingRect = children[0].getBoundingClientRect(); switch (bigAlignItems) { case 'start': expect(boundingRect.top).toBeCloseTo(0, 1); break; case 'end': expect(boundingRect.top).toBeCloseTo(60, 1); break; case 'center': default: expect(boundingRect.top).toBeCloseTo(30, 1); break; } }); }); }); describe('small and big maxWidth and maxHeight', () => { it('does not go over the bigMaxWidth and bigMaxHeight and stays centered', () => { const layoutContainer = initLayoutContainer(layoutDiv, { bigMaxWidth: 200, bigMaxHeight: 220, }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); expect(div1Rect.width).toBeCloseTo(200, 1); expect(div1Rect.height).toBeCloseTo(220, 1); expect(div1Rect.left).toBeCloseTo(60, 1); expect(div1Rect.top).toBeCloseTo(40, 1); const div2Rect = div2.getBoundingClientRect(); expect(div2Rect.width).toBeCloseTo(80, 1); expect(div2Rect.height).toBeCloseTo(120, 1); expect(div2Rect.left).toBeCloseTo(320, 1); expect(div2Rect.top).toBeCloseTo(90, 1); }); it('does not go over the smallMaxWidth and smallMaxHeight and stays centered', () => { const layoutContainer = initLayoutContainer(layoutDiv, { smallMaxWidth: 50, smallMaxHeight: 50, }); layoutContainer.layout(); const div1Rect = div1.getBoundingClientRect(); expect(div1Rect.width).toBeCloseTo(320, 1); expect(div1Rect.height).toBeCloseTo(300, 1); expect(div1Rect.left).toBeCloseTo(0, 1); expect(div1Rect.top).toBeCloseTo(0, 1); const div2Rect = div2.getBoundingClientRect(); expect(div2Rect.width).toBeCloseTo(50, 1); expect(div2Rect.height).toBeCloseTo(50, 1); expect(div2Rect.left).toBeCloseTo(335, 1); expect(div2Rect.top).toBeCloseTo(125, 1); }); }); }); }); describe('handling layout of 4 elements', () => { let layoutDiv; let divs = []; const divCount = 4; beforeEach(() => { layoutDiv = document.createElement('div'); layoutDiv.setAttribute('id', 'layoutDiv'); layoutDiv.style.position = 'absolute'; layoutDiv.style.top = '0px'; layoutDiv.style.left = '0px'; layoutDiv.style.width = '1280px'; layoutDiv.style.height = '720px'; layoutDiv.style.backgroundColor = 'grey'; document.body.style.margin = '0px'; document.body.style.padding = '0px'; document.body.appendChild(layoutDiv); const colors = ['blue', 'green', 'orange', 'teal']; for (let i = 0; i < divCount; i += 1) { divs[i] = document.createElement('div'); divs[i].style.backgroundColor = colors[i]; layoutDiv.appendChild(divs[i]); } }); afterEach(() => { document.body.removeChild(layoutDiv); layoutDiv = null; divs = []; }); it('favours an even distribution of elements in each row with evenRows true', () => { const layoutContainer = initLayoutContainer(layoutDiv, { maxWidth: 400, maxHeight: 300, evenRows: true, }); layoutContainer.layout(); // Expect them to all have the same width and height let rect; for (let i = 0; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); expect(rect.width).toBe(400); expect(rect.height).toBe(300); } // 2 on each row rect = divs[0].getBoundingClientRect(); expect(rect.left).toBeCloseTo(240, 1); expect(rect.top).toBeCloseTo(60, 1); rect = divs[1].getBoundingClientRect(); expect(rect.left).toBeCloseTo(640, 1); expect(rect.top).toBeCloseTo(60, 1); rect = divs[2].getBoundingClientRect(); expect(rect.left).toBeCloseTo(240, 1); expect(rect.top).toBeCloseTo(360, 1); rect = divs[3].getBoundingClientRect(); expect(rect.left).toBeCloseTo(640, 1); expect(rect.top).toBeCloseTo(360, 1); }); it('does not favours an even distribution of elements in each row with evenRows false', () => { const layoutContainer = initLayoutContainer(layoutDiv, { maxWidth: 400, maxHeight: 300, evenRows: false, }); layoutContainer.layout(); // Expect them to all have the same width and height let rect; for (let i = 0; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); expect(rect.width).toBe(400); expect(rect.height).toBe(300); } // 3 on same row, 1 on next row rect = divs[0].getBoundingClientRect(); expect(rect.left).toBeCloseTo(40, 1); expect(rect.top).toBeCloseTo(60, 1); rect = divs[1].getBoundingClientRect(); expect(rect.left).toBeCloseTo(440, 1); expect(rect.top).toBeCloseTo(60, 1); rect = divs[2].getBoundingClientRect(); expect(rect.left).toBeCloseTo(840, 1); expect(rect.top).toBeCloseTo(60, 1); rect = divs[3].getBoundingClientRect(); expect(rect.left).toBeCloseTo(440, 1); expect(rect.top).toBeCloseTo(360, 1); }); }); describe('handling layout of 5 elements', () => { let layoutDiv; let divs = []; const divCount = 5; beforeEach(() => { layoutDiv = document.createElement('div'); layoutDiv.setAttribute('id', 'layoutDiv'); layoutDiv.style.position = 'absolute'; layoutDiv.style.top = '0px'; layoutDiv.style.left = '0px'; layoutDiv.style.width = '400px'; layoutDiv.style.height = '300px'; layoutDiv.style.backgroundColor = 'grey'; document.body.style.margin = '0px'; document.body.style.padding = '0px'; document.body.appendChild(layoutDiv); const colors = ['blue', 'green', 'orange', 'teal', 'yellow']; for (let i = 0; i < divCount; i += 1) { divs[i] = document.createElement('div'); divs[i].style.backgroundColor = colors[i]; layoutDiv.appendChild(divs[i]); } }); afterEach(() => { document.body.removeChild(layoutDiv); layoutDiv = null; divs = []; }); it('handles default layout', () => { const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); // Expect them to all have the same width and height let rect; for (let i = 0; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); expect(rect.width).toBeCloseTo(133, 1); expect(rect.height).toBeCloseTo(150, 1); } rect = divs[0].getBoundingClientRect(); expect(rect.left).toBeCloseTo(0.5, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[1].getBoundingClientRect(); expect(rect.left).toBeCloseTo(133.5, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[2].getBoundingClientRect(); expect(rect.left).toBeCloseTo(266.5, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[3].getBoundingClientRect(); expect(rect.left).toBeCloseTo(67, 1); expect(rect.top).toBeCloseTo(150, 1); rect = divs[4].getBoundingClientRect(); expect(rect.left).toBeCloseTo(200, 1); expect(rect.top).toBeCloseTo(150, 1); }); it('grows to takes up the whole height if there are narrow elements', () => { divs[1].videoWidth = 480; divs[1].videoHeight = 640; divs[2].videoWidth = 1280; divs[2].videoHeight = 720; layoutDiv.style.width = '400px'; layoutDiv.style.height = '600px'; const layoutContainer = initLayoutContainer(layoutDiv, { fixedRatio: true }); layoutContainer.layout(); const rect = divs[4].getBoundingClientRect(); expect(rect.top + rect.height).toBeCloseTo(600, 1); }); it('allows some elements to be fixedRatio and some not', () => { divs[1].videoWidth = 480; divs[1].videoHeight = 640; divs[1].className = 'OT_fixedRatio'; divs[2].videoWidth = 1280; divs[2].videoHeight = 720; divs[2].className = 'OT_fixedRatio'; const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); const rect = divs[1].getBoundingClientRect(); expect(rect.width / rect.height).toBeCloseTo(480 / 640, 1); const rect2 = divs[2].getBoundingClientRect(); expect(rect2.width / rect2.height).toBeCloseTo(1280 / 720, 1); const rect3 = divs[3].getBoundingClientRect(); expect(rect3.width / rect3.height).not.toBeCloseTo(1280 / 720, 1); expect(rect3.width / rect3.height).not.toBeCloseTo(480 / 640, 1); }); it('does not grows to take up the whole height if you pass scaleLastRow=false', () => { divs[1].videoWidth = 480; divs[1].videoHeight = 640; divs[2].videoWidth = 1280; divs[2].videoHeight = 720; layoutDiv.style.width = '400px'; layoutDiv.style.height = '600px'; const layoutContainer = initLayoutContainer(layoutDiv, { fixedRatio: true, scaleLastRow: false, }); layoutContainer.layout(); const rect = divs[4].getBoundingClientRect(); expect(rect.top + rect.height).toBeCloseTo(514, 1); }); it('does not mess up the aspect ratio of the last row when it grows', () => { layoutDiv.style.width = '600px'; layoutDiv.style.height = '500px'; const layoutContainer = initLayoutContainer(layoutDiv, { minRatio: 1, maxRatio: 1, }); layoutContainer.layout(); const rect = divs[4].getBoundingClientRect(); expect(rect.width / rect.height).toBe(1); }); it('handles a big element', () => { divs[0].className = 'OT_big'; const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); // Expect div[0] to be big const bigRect = divs[0].getBoundingClientRect(); expect(bigRect.width).toBeCloseTo(320, 1); expect(bigRect.height).toBeCloseTo(300, 1); expect(bigRect.left).toBeCloseTo(0, 1); expect(bigRect.top).toBeCloseTo(0, 1); // Expect them to all have the same width and height let rect; for (let i = 1; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); expect(rect.width).toBeCloseTo(80, 1); expect(rect.height).toBeCloseTo(75, 1); } rect = divs[1].getBoundingClientRect(); expect(rect.left).toBeCloseTo(320, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[2].getBoundingClientRect(); expect(rect.left).toBeCloseTo(320, 1); expect(rect.top).toBeCloseTo(75, 1); rect = divs[3].getBoundingClientRect(); expect(rect.left).toBeCloseTo(320, 1); expect(rect.top).toBeCloseTo(150, 1); rect = divs[4].getBoundingClientRect(); expect(rect.left).toBeCloseTo(320, 1); expect(rect.top).toBeCloseTo(225, 1); }); it('handles two big elements', () => { divs[0].className = 'OT_big'; divs[1].className = 'OT_big'; const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); // Expect div[0] to be big const bigRect = divs[0].getBoundingClientRect(); expect(bigRect.width).toBeCloseTo(266.66, 1); expect(bigRect.height).toBeCloseTo(150, 1); expect(bigRect.left).toBeCloseTo(26.66, 1); expect(bigRect.top).toBeCloseTo(0, 1); // Expect div[1] to be big const big2Rect = divs[1].getBoundingClientRect(); expect(big2Rect.width).toBeCloseTo(266.66, 1); expect(big2Rect.height).toBeCloseTo(150, 1); expect(big2Rect.left).toBeCloseTo(26.66, 1); expect(big2Rect.top).toBeCloseTo(150, 1); // Expect them to all have the same width and height let rect; for (let i = 2; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); expect(rect.width).toBeCloseTo(80, 1); expect(rect.height).toBeCloseTo(100, 1); } rect = divs[2].getBoundingClientRect(); expect(rect.left).toBeCloseTo(320, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[3].getBoundingClientRect(); expect(rect.left).toBeCloseTo(320, 1); expect(rect.top).toBeCloseTo(100, 1); rect = divs[4].getBoundingClientRect(); expect(rect.left).toBeCloseTo(320, 1); expect(rect.top).toBeCloseTo(200, 1); }); it('handles hidden elements', () => { divs[0].style.display = 'none'; const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); for (let i = 1; i < divs.length; i += 1) { const rect = divs[i].getBoundingClientRect(); expect(rect.width).toBeCloseTo(200, 1); expect(rect.height).toBeCloseTo(150, 1); } }); describe('in really wide div', () => { beforeEach(() => { layoutDiv.style.width = '1000px'; }); it('handles default layout', () => { const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); // Expect them to all have the same width and height let rect; for (let i = 0; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); expect(rect.width).toBeCloseTo(200, 1); expect(rect.height).toBeCloseTo(300, 1); } rect = divs[0].getBoundingClientRect(); expect(rect.left).toBeCloseTo(0, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[1].getBoundingClientRect(); expect(rect.left).toBeCloseTo(200, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[2].getBoundingClientRect(); expect(rect.left).toBeCloseTo(400, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[3].getBoundingClientRect(); expect(rect.left).toBeCloseTo(600, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[4].getBoundingClientRect(); expect(rect.left).toBeCloseTo(800, 1); expect(rect.top).toBeCloseTo(0, 1); }); it('handles a big element', () => { divs[0].className = 'OT_big'; const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); // Expect div[0] to be big const bigRect = divs[0].getBoundingClientRect(); expect(bigRect.width).toBeCloseTo(533.33, 1); expect(bigRect.height).toBeCloseTo(300, 1); expect(bigRect.left).toBeCloseTo(133.33, 1); expect(bigRect.top).toBeCloseTo(0, 1); // Expect them to all have the same width and height let rect; for (let i = 1; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); expect(rect.width).toBeCloseTo(100, 1); expect(rect.height).toBeCloseTo(150, 1); } rect = divs[1].getBoundingClientRect(); expect(rect.left).toBeCloseTo(800, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[2].getBoundingClientRect(); expect(rect.left).toBeCloseTo(900, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[3].getBoundingClientRect(); expect(rect.left).toBeCloseTo(800, 1); expect(rect.top).toBeCloseTo(150, 1); rect = divs[4].getBoundingClientRect(); expect(rect.left).toBeCloseTo(900, 1); expect(rect.top).toBeCloseTo(150, 1); }); it('handles bigFirst "column" and "row"', () => { divs[0].className = 'OT_big'; let layoutContainer = initLayoutContainer(layoutDiv, { bigFirst: 'column' }); layoutContainer.layout(); // Big element is displayed on the left because we are in a column layout let bigDivRect = divs[0].getBoundingClientRect(); expect(bigDivRect.left).toBeCloseTo(133.33); expect(bigDivRect.top).toBe(0); layoutContainer = initLayoutContainer(layoutDiv, { bigFirst: 'row' }); layoutContainer.layout(); // Big element is displayed on the right because we are in a column layout bigDivRect = divs[0].getBoundingClientRect(); expect(bigDivRect.left).toBeCloseTo(333.33); expect(bigDivRect.top).toBe(0); }); it('handles two big elements', () => { divs[0].className = 'OT_big'; divs[1].className = 'OT_big'; const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); // Expect div[0] to be big const bigRect = divs[0].getBoundingClientRect(); expect(bigRect.width).toBeCloseTo(400, 1); expect(bigRect.height).toBeCloseTo(300, 1); expect(bigRect.left).toBeCloseTo(0, 1); expect(bigRect.top).toBeCloseTo(0, 1); // Expect div[1] to be big const big2Rect = divs[1].getBoundingClientRect(); expect(big2Rect.width).toBeCloseTo(400, 1); expect(big2Rect.height).toBeCloseTo(300, 1); expect(big2Rect.left).toBeCloseTo(400, 1); expect(big2Rect.top).toBeCloseTo(0, 1); // Expect them to all have the same width and height let rect; for (let i = 2; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); expect(rect.width).toBeCloseTo(177.76, 1); expect(rect.height).toBeCloseTo(100, 1); } rect = divs[2].getBoundingClientRect(); expect(rect.left).toBeCloseTo(811.1, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[3].getBoundingClientRect(); expect(rect.left).toBeCloseTo(811.1, 1); expect(rect.top).toBeCloseTo(100, 1); rect = divs[4].getBoundingClientRect(); expect(rect.left).toBeCloseTo(811.1, 1); expect(rect.top).toBeCloseTo(200, 1); }); it('with fixedRatio does not scale the big percentage down too small and leave floating elements', () => { // If you set the width to be wide and have a portrait aspect ratio divs[0].className = 'OT_big'; divs.forEach((div) => { div.videoWidth = 360; div.videoHeight = 480; }); const layoutContainer = initLayoutContainer(layoutDiv, { bigPercentage: 0.9, minBigPercentage: 0.6, smallMaxWidth: 300, smallMaxHeight: 300, fixedRatio: true, }); layoutContainer.layout(); // Divs are right up against the edge let maxRight = 0; for (let i = 1; i < divs.length; i += 1) { const rect = divs[i].getBoundingClientRect(); maxRight = Math.max(maxRight, rect.left + rect.width); } expect(maxRight).toBeCloseTo(1000, -1); }); }); describe('in really tall div', () => { beforeEach(() => { layoutDiv.style.height = '800px'; }); it('handles default layout', () => { const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); // Expect them to all have the same width and height let rect; for (let i = 0; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); if (i > divs.length - 2) { // The last row grows a little bit to take up the extra space expect(rect.height).toBeCloseTo(268, 1); expect(rect.width).toBeCloseTo(201, 1); } else { expect(rect.height).toBeCloseTo(266, 1); expect(rect.width).toBeCloseTo(200, 1); } } rect = divs[0].getBoundingClientRect(); expect(rect.left).toBeCloseTo(0, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[1].getBoundingClientRect(); expect(rect.left).toBeCloseTo(200, 1); expect(rect.top).toBeCloseTo(0, 1); rect = divs[2].getBoundingClientRect(); expect(rect.left).toBeCloseTo(0, 1); expect(rect.top).toBeCloseTo(266, 1); rect = divs[3].getBoundingClientRect(); expect(rect.left).toBeCloseTo(200, 1); expect(rect.top).toBeCloseTo(266, 1); rect = divs[4].getBoundingClientRect(); expect(rect.left).toBeCloseTo(99.5, 1); expect(rect.top).toBeCloseTo(532, 0); }); it('handles a big element', () => { divs[0].className = 'OT_big'; const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); // Expect div[0] to be big const bigRect = divs[0].getBoundingClientRect(); expect(bigRect.width).toBeCloseTo(400, 1); expect(bigRect.height).toBeCloseTo(600, 1); expect(bigRect.left).toBeCloseTo(0, 1); expect(bigRect.top).toBeCloseTo(20, 1); // Expect them to all have the same width and height let rect; for (let i = 1; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); expect(rect.width).toBeCloseTo(100, 1); expect(rect.height).toBeCloseTo(150, 1); } rect = divs[1].getBoundingClientRect(); expect(rect.left).toBeCloseTo(0, 1); expect(rect.top).toBeCloseTo(645, 1); rect = divs[2].getBoundingClientRect(); expect(rect.left).toBeCloseTo(100, 1); expect(rect.top).toBeCloseTo(645, 1); rect = divs[3].getBoundingClientRect(); expect(rect.left).toBeCloseTo(200, 1); expect(rect.top).toBeCloseTo(645, 1); rect = divs[4].getBoundingClientRect(); expect(rect.left).toBeCloseTo(300, 1); expect(rect.top).toBeCloseTo(645, 1); }); it('handles bigFirst "column" and "row"', () => { divs[0].className = 'OT_big'; let layoutContainer = initLayoutContainer(layoutDiv, { bigFirst: 'column' }); layoutContainer.layout(); // Big element is displayed at the bottom because we are in a row layout let bigDivRect = divs[0].getBoundingClientRect(); expect(bigDivRect.left).toBeCloseTo(0, 1); expect(bigDivRect.top).toBeCloseTo(180, 1); layoutContainer = initLayoutContainer(layoutDiv, { bigFirst: 'row' }); layoutContainer.layout(); // Big element is displayed at the top because we are in a row layout bigDivRect = divs[0].getBoundingClientRect(); expect(bigDivRect.left).toBeCloseTo(0, 1); expect(bigDivRect.top).toBeCloseTo(20, 1); }); it('handles two big elements', () => { divs[0].className = 'OT_big'; divs[1].className = 'OT_big'; const layoutContainer = initLayoutContainer(layoutDiv); layoutContainer.layout(); // Expect div[0] to be big const bigRect = divs[0].getBoundingClientRect(); expect(bigRect.width).toBeCloseTo(400, 1); expect(bigRect.height).toBeCloseTo(320, 1); expect(bigRect.left).toBeCloseTo(0, 1); expect(bigRect.top).toBeCloseTo(0, 1); // Expect div[1] to be big const big2Rect = divs[1].getBoundingClientRect(); expect(big2Rect.width).toBeCloseTo(400, 1); expect(big2Rect.height).toBeCloseTo(320, 1); expect(big2Rect.left).toBeCloseTo(0, 1); expect(big2Rect.top).toBeCloseTo(320, 1); // Expect them to all have the same width and height let rect; for (let i = 2; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); expect(rect.width).toBeCloseTo(133, 1); expect(rect.height).toBeCloseTo(160, 1); } rect = divs[2].getBoundingClientRect(); expect(rect.left).toBeCloseTo(0.5, 1); expect(rect.top).toBeCloseTo(640, 1); rect = divs[3].getBoundingClientRect(); expect(rect.left).toBeCloseTo(133.5, 1); expect(rect.top).toBeCloseTo(640, 1); rect = divs[4].getBoundingClientRect(); expect(rect.left).toBeCloseTo(266.5, 1); expect(rect.top).toBeCloseTo(640, 1); }); it('with fixedRatio does not scale the big percentage down too small and leave floating elements', () => { // If you set the width to be wide and have a portrait aspect ratio divs[0].className = 'OT_big'; divs.forEach((div) => { div.videoWidth = 1280; div.videoHeight = 720; }); const layoutContainer = initLayoutContainer(layoutDiv, { bigPercentage: 0.9, minBigPercentage: 0.6, smallMaxWidth: 300, smallMaxHeight: 300, fixedRatio: true, }); layoutContainer.layout(); // Divs are right up against the edge let maxBottom = 0; for (let i = 1; i < divs.length; i += 1) { const rect = divs[i].getBoundingClientRect(); maxBottom = Math.max(maxBottom, rect.top + rect.height); } expect(maxBottom).toBeCloseTo(800, 1); }); }); }); function testLotsOfElements(howMany) { describe(`handling layout of ${howMany} elements`, () => { let layoutDiv; let divs = []; const divCount = 23; beforeEach(() => { layoutDiv = document.createElement('div'); layoutDiv.setAttribute('id', 'layoutDiv'); layoutDiv.style.position = 'absolute'; layoutDiv.style.top = '0px'; layoutDiv.style.left = '0px'; layoutDiv.style.width = '400px'; layoutDiv.style.height = '300px'; layoutDiv.style.backgroundColor = 'grey'; document.body.style.margin = '0px'; document.body.style.padding = '0px'; document.body.appendChild(layoutDiv); for (let i = 0; i < divCount; i += 1) { divs[i] = document.createElement('div'); divs[i].style.backgroundColor = `rgb(${15 * i}, ${15 * i}, ${15 * i}`; divs[i].videoWidth = 360; divs[i].videoHeight = 720; layoutDiv.appendChild(divs[i]); } }); afterEach(() => { document.body.removeChild(layoutDiv); layoutDiv = null; divs = []; }); it('favours an even distribution of elements in each row with fixed Ratio', () => { const layoutContainer = initLayoutContainer(layoutDiv, { fixedRatio: true }); layoutContainer.layout(); // Expect there to be a close number of items on each row let rect; let currentRow = -1; let currentRowTop = -1; const rowItemCount = []; for (let i = 0; i < divs.length; i += 1) { rect = divs[i].getBoundingClientRect(); if (rect.top > currentRowTop) { currentRowTop = rect.top; currentRow += 1; } rowItemCount[currentRow] = (rowItemCount[currentRow] || 0) + 1; } for (let i = 0; i < rowItemCount.length - 1; i += 1) { expect(Math.abs(rowItemCount[i] - rowItemCount[i + 1])).toBeLessThan(2); } }); }); } testLotsOfElements(7); testLotsOfElements(13); testLotsOfElements(18); });