Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added wait if loader is present #1854

Closed
wants to merge 7 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -81,6 +81,7 @@ jobs:
name: dist
path: packages
- run: yarn
- run: sudo apt update
- name: Install browser dependencies
run: sudo apt-get install -y libgbm-dev
if: ${{ matrix.os == 'ubuntu-latest' }}
13 changes: 13 additions & 0 deletions packages/core/src/page.js
Original file line number Diff line number Diff line change
@@ -207,6 +207,19 @@ export class Page {
// serialize and capture a DOM snapshot
this.log.debug('Serialize DOM', this.meta);

const waitTime = parseInt(process.env.LOADER_WAIT_TIMEOUT) || 2000;

/* istanbul ignore next */
const shouldWait = await this.eval(async () => {
/* eslint-disable-next-line no-undef */
return PercyDOM.checkForLoader();
});
if (shouldWait) {
await new Promise(resolve => {
setTimeout(resolve, waitTime);
});
}

/* istanbul ignore next: no instrumenting injected code */
let capture = await this.eval((_, options) => ({
/* eslint-disable-next-line no-undef */
46 changes: 44 additions & 2 deletions packages/core/test/percy.test.js
Original file line number Diff line number Diff line change
@@ -105,8 +105,9 @@ describe('Percy', () => {
});

// expect required arguments are passed to PercyDOM.serialize
expect(evalSpy.calls.allArgs()[3]).toEqual(jasmine.arrayContaining([jasmine.anything(), { enableJavaScript: undefined, disableShadowDOM: true, domTransformation: undefined, reshuffleInvalidTags: undefined }]));

expect(evalSpy.calls.allArgs()[3][0]).toEqual(jasmine.any(Function));
expect(evalSpy.calls.allArgs()[3][0].constructor.name).toBe('AsyncFunction');
expect(evalSpy.calls.allArgs()[4]).toEqual(jasmine.arrayContaining([jasmine.anything(), { enableJavaScript: undefined, disableShadowDOM: true, domTransformation: undefined, reshuffleInvalidTags: undefined }]));
expect(snapshot.url).toEqual('http://localhost:8000/');
expect(snapshot.domSnapshot).toEqual(jasmine.objectContaining({
html: '<!DOCTYPE html><html><head></head><body>' + (
@@ -115,6 +116,47 @@ describe('Percy', () => {
}));
});

it('should wait for n seconds if a loader is detected and env variable is present', async () => {
server.reply('/', () => [
200,
'text/html',
`<!DOCTYPE html><html><head></head><body><div style="width: 800px; height: 600px; display: block; visibility: visible; opacity: 1;" class="parent">
<div style="width: 600px; height: 500px; display: block; visibility: visible; opacity: 1;" class="loader">
<div></div>
</div>
</div></body></html>`
]);
process.env.LOADER_WAIT_TIMEOUT = 4000;
await percy.browser.launch();
let page = await percy.browser.page();
await page.goto('http://localhost:8000');

let startTime = Date.now();
await page.snapshot({ disableShadowDOM: true });
let endTime = Date.now();
expect(endTime - startTime).toBeGreaterThanOrEqual(4000);
});

it('should wait for 2 seconds if loader is present but no env variable', async () => {
server.reply('/', () => [
200,
'text/html',
`<!DOCTYPE html><html><head></head><body><div style="width: 800px; height: 600px; display: block; visibility: visible; opacity: 1;" class="parent">
<div style="width: 600px; height: 500px; display: block; visibility: visible; opacity: 1;" class="loader">
<div></div>
</div>
</div></body></html>`
]);
await percy.browser.launch();
let page = await percy.browser.page();
await page.goto('http://localhost:8000');

let startTime = Date.now();
await page.snapshot({ disableShadowDOM: true });
let endTime = Date.now();
expect(endTime - startTime).toBeGreaterThanOrEqual(2000);
});

describe('.start()', () => {
// rather than stub prototypes, extend and mock
class TestPercy extends Percy {
67 changes: 67 additions & 0 deletions packages/dom/src/check-dom-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Determines if an element is visible
export function isElementVisible(el) {
const style = window.getComputedStyle(el);
return (
style.display !== 'none' &&
style.visibility !== 'hidden' &&
parseFloat(style.opacity) > 0
);
}

// Checks if an element is a loader element by traversing its children
export function isLoaderElement(el, maxDepth = 2, currentDepth = 0) {
if (currentDepth >= maxDepth) return false;

const children = el.children;
if (children.length === 0) return true;

for (let i = 0; i < children.length; i++) {
if (!isLoaderElement(children[i], maxDepth, currentDepth + 1)) return false;
}

return true;
}

// Checks for loader elements in the DOM
export function checkForLoader() {
const loaders = Array.from(document.querySelectorAll('*')).filter(el =>
(typeof el.className === 'string' && el.className.includes('loader')) ||
(typeof el.id === 'string' && el.id.includes('loader'))
);

return loaders.some(loader => {
const parent = loader.parentElement;

if (!isElementVisible(loader) || !isElementVisible(parent)) return false;
if (!isLoaderElement(loader)) return false;

const parentRect = parent.getBoundingClientRect();
const loaderRect = loader.getBoundingClientRect();

const viewportWidth = window.innerWidth;
const viewportHeight = Math.max(
document.documentElement.scrollHeight,
window.innerHeight
);

if (parentRect.width > loaderRect.width && parentRect.height > loaderRect.height) {
const widthPercentage = (parentRect.width / viewportWidth) * 100;
const heightPercentage = (parentRect.height / viewportHeight) * 100;

if (widthPercentage >= 75 && heightPercentage >= 75) {
return true;
}
} else {
const widthPercentage = (loaderRect.width / viewportWidth) * 100;
const heightPercentage = (loaderRect.height / viewportHeight) * 100;

if (widthPercentage >= 75 && heightPercentage >= 75) {
return true;
}
}

return false;
});
}

export default { checkForLoader };
1 change: 1 addition & 0 deletions packages/dom/src/index.js
Original file line number Diff line number Diff line change
@@ -7,3 +7,4 @@ export {
} from './serialize-dom';

export { loadAllSrcsetLinks } from './serialize-image-srcset';
export { checkForLoader } from './check-dom-loader';
99 changes: 99 additions & 0 deletions packages/dom/test/check-dom-loader.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { checkForLoader } from '../src/check-dom-loader';
import { withExample } from './helpers';

describe('checkForLoader', () => {
let div, loaderElement;

beforeEach(() => {
withExample('<div id="content"></div>', { showLoader: true });

loaderElement = document.querySelector('.loader');
loaderElement.style.display = 'block';
loaderElement.style.visibility = 'visible';
loaderElement.style.opacity = '1';
div = document.querySelector('.parent');
div.style.display = 'block';
div.style.visibility = 'visible';
div.style.opacity = '1';
});

afterEach(() => {
loaderElement = null;
div = null;
});

it('should return true if the loader is visible and meets the size percentage criteria', () => {
loaderElement.style.width = '800px';
loaderElement.style.height = '600px';
const result = checkForLoader();
expect(result).toBe(true);
});

it('should return true if parent meets the size percentage criteria', () => {
div.style.width = '800px';
div.style.height = '3000px';
loaderElement.style.width = '600px';
loaderElement.style.height = '500px';

const result = checkForLoader();
expect(result).toBe(true);
});

it('should return false if one of percentage criteria fails', () => {
div.style.width = '800px';
div.style.height = '200px';
loaderElement.style.width = '600px';
loaderElement.style.height = '500px';

const result = checkForLoader();
expect(result).toBe(false);
});

it('should return true if loader has upto depth 1 children', () => {
const child1 = document.createElement('div');
div.style.height = '6000px';
loaderElement.appendChild(child1);
const result = checkForLoader();
expect(result).toBe(true);
});

it('should return false if the loader element is not visible', () => {
loaderElement.style.visibility = 'hidden';

const result = checkForLoader();
expect(result).toBe(false);
});

it('should return false if the loader element is inside an invisible parent', () => {
div.style.visibility = 'hidden';

const result = checkForLoader();
expect(result).toBe(false);
});

it('should return false if the loader does not meet the size percentage criteria', () => {
div.style.width = '200px';
div.style.height = '200px';
loaderElement.style.width = '100px';
loaderElement.style.height = '100px';

const result = checkForLoader();
expect(result).toBe(false);
});

it('should return false if no loader element is found', () => {
div.removeChild(loaderElement);

const result = checkForLoader();
expect(result).toBe(false);
});

it('should return false if loader has upto depth 3 children', () => {
const child1 = document.createElement('div');
const child2 = document.createElement('div');
child1.appendChild(child2);
loaderElement.appendChild(child1);
const result = checkForLoader();
expect(result).toBe(false);
});
});
18 changes: 17 additions & 1 deletion packages/dom/test/helpers.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ export const chromeBrowser = 'CHROME';
export const firefoxBrowser = 'FIREFOX';

// create and cleanup testing DOM
export function withExample(html, options = { withShadow: true, withRestrictedShadow: false, invalidTagsOutsideBody: false }) {
export function withExample(html, options = { withShadow: true, withRestrictedShadow: false, invalidTagsOutsideBody: false, showLoader: false }) {
let $test = document.getElementById('test');
if ($test) $test.remove();

@@ -25,6 +25,22 @@ export function withExample(html, options = { withShadow: true, withRestrictedSh
document.body.appendChild($testShadow);
}

// Add loader if the option is true
if (options.showLoader) {
const parentElement = document.createElement('div');
document.body.appendChild(parentElement);
parentElement.style.width = '800px';
parentElement.style.height = '600px';
parentElement.classList.add('parent');
const loaderElement = document.createElement('div');
loaderElement.style.width = '600px';
loaderElement.style.height = '500px';
loaderElement.classList.add('loader');
parentElement.appendChild(loaderElement);
window.innerWidth = 1024;
window.innerHeight = 768;
}

if (options.withRestrictedShadow) {
$testShadow = document.createElement('div');
$testShadow.id = 'test-shadow';