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:
- with a callback (preferred),
waitFor(action)
......@@ -8,61 +30,17 @@
- with await,
await waitFor;
action();
*/
const CSS_LOADED_EVENT = 'CSSLoaded';
const DOM_LOADED_EVENT = 'DOMContentLoaded';
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();
}
};
};
export const handleStartupEvents = (action = () => {}) => {
if (!gon.features.startupCss) {
return action;
}
const startupLinks = Array.from(document.querySelectorAll('link[data-startupcss]'));
return () => {
if (startupLinks.every(isStartupLinkLoaded)) {
-*/
export const waitForCSSLoaded = (action = () => {}) => {
if (!gon.features.startupCss || allLinksLoaded()) {
return new Promise(resolve => {
action();
}
};
};
export const waitForStartupLinks = () => {
let eventListener;
const promise = new Promise(resolve => {
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);
resolve();
});
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 });
return new Promise(resolve => {
document.addEventListener(CSS_LOADED_EVENT, resolve, { once: true });
document.addEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents);
}).then(action);
waitForStartupLinks();
return promise;
};
import {
handleLoadedEvents,
waitForCSSLoaded,
} from '../../../app/assets/javascripts/helpers/startup_css_helper';
describe('handleLoadedEvents', () => {
let mock;
describe('waitForCSSLoaded', () => {
let mockedCallback;
beforeEach(() => {
mock = jest.fn();
mockedCallback = 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();
describe('Promise-like api', () => {
it('can be used with a callback', async () => {
await waitForCSSLoaded(mockedCallback);
expect(mockedCallback).toHaveBeenCalledTimes(1);
});
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);
it('can be used as a promise', async () => {
await waitForCSSLoaded().then(mockedCallback);
expect(mockedCallback).toHaveBeenCalledTimes(1);
});
});
describe('waitForCSSLoaded', () => {
let mock;
beforeEach(() => {
mock = jest.fn();
});
describe('with startup css disabled', () => {
beforeEach(() => {
gon.features = {
startupCss: false,
};
});
it('should call CssLoaded when the conditions are met', async () => {
const docAddListener = jest.spyOn(document, 'addEventListener');
const docRemoveListener = jest.spyOn(document, 'removeEventListener');
const docDispatch = jest.spyOn(document, 'dispatchEvent');
const events = waitForCSSLoaded(mock);
expect(docAddListener).toHaveBeenCalledTimes(3);
expect(docDispatch.mock.calls[0][0].type).toBe('CSSStartupLinkLoaded');
document.dispatchEvent(new CustomEvent('DOMContentLoaded'));
it('should invoke the action right away', async () => {
const events = waitForCSSLoaded(mockedCallback);
await events;
expect(docDispatch).toHaveBeenCalledTimes(3);
expect(docDispatch.mock.calls[2][0].type).toBe('CSSLoaded');
expect(docRemoveListener).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledTimes(1);
expect(mockedCallback).toHaveBeenCalledTimes(1);
});
});
describe('with startup css enabled', () => {
let docAddListener;
let docRemoveListener;
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 () => {
const events = waitForCSSLoaded(mock);
const fixtures = `
it('should dispatch CSSLoaded when the assets are cached or already loaded', async () => {
setFixtures(`
<link href="one.css" data-startupcss="loaded">
<link href="two.css" data-startupcss="loaded">
`;
setFixtures(fixtures);
expect(docAddListener).toHaveBeenCalledTimes(3);
expect(docDispatch.mock.calls[0][0].type).toBe('CSSStartupLinkLoaded');
`);
await waitForCSSLoaded(mockedCallback);
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);
expect(mockedCallback).toHaveBeenCalledTimes(1);
});
it('should wait to call CssLoaded until the assets are loaded', async () => {
const events = waitForCSSLoaded(mock);
const fixtures = `
setFixtures(`
<link href="one.css" data-startupcss="loading">
<link href="two.css" data-startupcss="loading">
`;
setFixtures(fixtures);
expect(docAddListener).toHaveBeenCalledTimes(3);
expect(docDispatch.mock.calls[0][0].type).toBe('CSSStartupLinkLoaded');
`);
const events = waitForCSSLoaded(mockedCallback);
document
.querySelectorAll('[data-startupcss="loading"]')
.forEach(elem => elem.setAttribute('data-startupcss', 'loaded'));
document.dispatchEvent(new CustomEvent('DOMContentLoaded'));
document.dispatchEvent(new CustomEvent('CSSStartupLinkLoaded'));
await events;
expect(docDispatch).toHaveBeenCalledTimes(4);
expect(docDispatch.mock.calls[3][0].type).toBe('CSSLoaded');
expect(docRemoveListener).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledTimes(1);
expect(mockedCallback).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