Commit daecc94d authored by André Luís's avatar André Luís

Simplify waitForCSSLoaded and support post-DCL use

This includes work from Angelo Gulina.

Makes waitForCSSLoaded not depend on
DOMContentLoaded (DCL).

Also adds support for calling it after DCL
has been fired. This makes for easier and more
reliable usage.
parent 698cf2cc
const CSS_LOADED_EVENT = 'CSSLoaded';
const STARTUP_LINK_LOADED_EVENT = 'CSSStartupLinkLoaded';
const getAllStartupLinks = (() => {
let links = null;
return () => {
if (!links) {
links = Array.from(document.querySelectorAll('link[data-startupcss]'));
}
return links;
};
})();
const isStartupLinkLoaded = ({ dataset }) => dataset.startupcss === 'loaded';
const allLinksLoaded = () => getAllStartupLinks().every(isStartupLinkLoaded);
const handleStartupEvents = () => {
if (allLinksLoaded()) {
document.dispatchEvent(new CustomEvent(CSS_LOADED_EVENT));
document.removeEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents);
}
};
/* Wait for.... The methods can be used: /* Wait for.... The methods can be used:
- with a callback (preferred), - with a callback (preferred),
waitFor(action) waitFor(action)
...@@ -8,61 +30,17 @@ ...@@ -8,61 +30,17 @@
- with await, - with await,
await waitFor; await waitFor;
action(); action();
*/ -*/
export const waitForCSSLoaded = (action = () => {}) => {
const CSS_LOADED_EVENT = 'CSSLoaded'; if (!gon.features.startupCss || allLinksLoaded()) {
const DOM_LOADED_EVENT = 'DOMContentLoaded'; return new Promise(resolve => {
const STARTUP_LINK_LOADED_EVENT = 'CSSStartupLinkLoaded';
const isStartupLinkLoaded = ({ dataset }) => dataset.startupcss === 'loaded';
export const handleLoadedEvents = (action = () => {}) => {
let isCssLoaded = false;
let eventsList = [CSS_LOADED_EVENT, DOM_LOADED_EVENT];
return ({ type } = {}) => {
eventsList = eventsList.filter(e => e !== type);
if (isCssLoaded) {
return;
}
if (!eventsList.length) {
isCssLoaded = true;
action(); action();
} resolve();
}; });
};
export const handleStartupEvents = (action = () => {}) => {
if (!gon.features.startupCss) {
return action;
} }
const startupLinks = Array.from(document.querySelectorAll('link[data-startupcss]'));
return () => {
if (startupLinks.every(isStartupLinkLoaded)) {
action();
}
};
};
export const waitForStartupLinks = () => { return new Promise(resolve => {
let eventListener; document.addEventListener(CSS_LOADED_EVENT, resolve, { once: true });
const promise = new Promise(resolve => { document.addEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents);
eventListener = handleStartupEvents(resolve);
document.addEventListener(STARTUP_LINK_LOADED_EVENT, eventListener);
}).then(() => {
document.dispatchEvent(new CustomEvent(CSS_LOADED_EVENT));
document.removeEventListener(STARTUP_LINK_LOADED_EVENT, eventListener);
});
document.dispatchEvent(new CustomEvent(STARTUP_LINK_LOADED_EVENT));
return promise;
};
export const waitForCSSLoaded = (action = () => {}) => {
let eventListener;
const promise = new Promise(resolve => {
eventListener = handleLoadedEvents(resolve);
document.addEventListener(DOM_LOADED_EVENT, eventListener, { once: true });
document.addEventListener(CSS_LOADED_EVENT, eventListener, { once: true });
}).then(action); }).then(action);
waitForStartupLinks();
return promise;
}; };
import { import {
handleLoadedEvents,
waitForCSSLoaded, waitForCSSLoaded,
} from '../../../app/assets/javascripts/helpers/startup_css_helper'; } from '../../../app/assets/javascripts/helpers/startup_css_helper';
describe('handleLoadedEvents', () => {
let mock;
beforeEach(() => {
mock = jest.fn();
});
it('should not call the callback on wrong conditions', () => {
const resolverToCall = handleLoadedEvents(mock);
resolverToCall({ type: 'UnrelatedEvent' });
resolverToCall({ type: 'UnrelatedEvent' });
resolverToCall({ type: 'UnrelatedEvent' });
resolverToCall({ type: 'UnrelatedEvent' });
resolverToCall({ type: 'CSSLoaded' });
resolverToCall();
expect(mock).not.toHaveBeenCalled();
});
it('should call the callback when all the events have been triggered', () => {
const resolverToCall = handleLoadedEvents(mock);
resolverToCall();
resolverToCall({ type: 'DOMContentLoaded' });
resolverToCall({ type: 'CSSLoaded' });
resolverToCall();
expect(mock).toHaveBeenCalledTimes(1);
});
});
describe('waitForCSSLoaded', () => { describe('waitForCSSLoaded', () => {
let mock; let mockedCallback;
beforeEach(() => { beforeEach(() => {
mock = jest.fn(); mockedCallback = jest.fn();
}); });
describe('with startup css disabled', () => { describe('Promise-like api', () => {
beforeEach(() => { it('can be used with a callback', async () => {
gon.features = { await waitForCSSLoaded(mockedCallback);
startupCss: false, expect(mockedCallback).toHaveBeenCalledTimes(1);
};
}); });
it('should call CssLoaded when the conditions are met', async () => { it('can be used as a promise', async () => {
const docAddListener = jest.spyOn(document, 'addEventListener'); await waitForCSSLoaded().then(mockedCallback);
const docRemoveListener = jest.spyOn(document, 'removeEventListener'); expect(mockedCallback).toHaveBeenCalledTimes(1);
const docDispatch = jest.spyOn(document, 'dispatchEvent'); });
const events = waitForCSSLoaded(mock); });
expect(docAddListener).toHaveBeenCalledTimes(3); describe('with startup css disabled', () => {
expect(docDispatch.mock.calls[0][0].type).toBe('CSSStartupLinkLoaded'); gon.features = {
startupCss: false,
};
document.dispatchEvent(new CustomEvent('DOMContentLoaded')); it('should invoke the action right away', async () => {
const events = waitForCSSLoaded(mockedCallback);
await events; await events;
expect(docDispatch).toHaveBeenCalledTimes(3); expect(mockedCallback).toHaveBeenCalledTimes(1);
expect(docDispatch.mock.calls[2][0].type).toBe('CSSLoaded');
expect(docRemoveListener).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledTimes(1);
}); });
}); });
describe('with startup css enabled', () => { describe('with startup css enabled', () => {
let docAddListener; gon.features = {
let docRemoveListener; startupCss: true,
let docDispatch; };
beforeEach(() => {
docAddListener = jest.spyOn(document, 'addEventListener');
docRemoveListener = jest.spyOn(document, 'removeEventListener');
docDispatch = jest.spyOn(document, 'dispatchEvent');
gon.features = {
startupCss: true,
};
});
it('should call CssLoaded if the assets are cached', async () => { it('should dispatch CSSLoaded when the assets are cached or already loaded', async () => {
const events = waitForCSSLoaded(mock); setFixtures(`
const fixtures = `
<link href="one.css" data-startupcss="loaded"> <link href="one.css" data-startupcss="loaded">
<link href="two.css" data-startupcss="loaded"> <link href="two.css" data-startupcss="loaded">
`; `);
setFixtures(fixtures); await waitForCSSLoaded(mockedCallback);
expect(docAddListener).toHaveBeenCalledTimes(3); expect(mockedCallback).toHaveBeenCalledTimes(1);
expect(docDispatch.mock.calls[0][0].type).toBe('CSSStartupLinkLoaded');
document.dispatchEvent(new CustomEvent('DOMContentLoaded'));
await events;
expect(docDispatch).toHaveBeenCalledTimes(3);
expect(docDispatch.mock.calls[2][0].type).toBe('CSSLoaded');
expect(docRemoveListener).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledTimes(1);
}); });
it('should wait to call CssLoaded until the assets are loaded', async () => { it('should wait to call CssLoaded until the assets are loaded', async () => {
const events = waitForCSSLoaded(mock); setFixtures(`
const fixtures = `
<link href="one.css" data-startupcss="loading"> <link href="one.css" data-startupcss="loading">
<link href="two.css" data-startupcss="loading"> <link href="two.css" data-startupcss="loading">
`; `);
setFixtures(fixtures); const events = waitForCSSLoaded(mockedCallback);
expect(docAddListener).toHaveBeenCalledTimes(3);
expect(docDispatch.mock.calls[0][0].type).toBe('CSSStartupLinkLoaded');
document document
.querySelectorAll('[data-startupcss="loading"]') .querySelectorAll('[data-startupcss="loading"]')
.forEach(elem => elem.setAttribute('data-startupcss', 'loaded')); .forEach(elem => elem.setAttribute('data-startupcss', 'loaded'));
document.dispatchEvent(new CustomEvent('DOMContentLoaded'));
document.dispatchEvent(new CustomEvent('CSSStartupLinkLoaded')); document.dispatchEvent(new CustomEvent('CSSStartupLinkLoaded'));
await events; await events;
expect(docDispatch).toHaveBeenCalledTimes(4); expect(mockedCallback).toHaveBeenCalledTimes(1);
expect(docDispatch.mock.calls[3][0].type).toBe('CSSLoaded');
expect(docRemoveListener).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledTimes(1);
}); });
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment