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
JavaScript
/* 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);
});