Commit 08b4ac19 authored by Angelo Gulina's avatar Angelo Gulina Committed by Tim Zallmann

Move charts initialisation further in time

When Startup.CSS feature is active, the charts will be initialised
when the application.css has been loaded to guarantee
the container display is not none
parent 4af7218d
/* Wait for.... The methods can be used:
- with a callback (preferred),
waitFor(action)
- with then (discouraged),
await waitFor().then(action);
- 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)) {
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);
});
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);
waitForStartupLinks();
return promise;
};
import Vue from 'vue';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { waitForCSSLoaded } from '../../../../helpers/startup_css_helper';
import { __ } from '~/locale';
import CodeCoverage from '../components/code_coverage.vue';
import SeriesDataMixin from './series_data_mixin';
document.addEventListener('DOMContentLoaded', () => {
waitForCSSLoaded(() => {
const languagesContainer = document.getElementById('js-languages-chart');
const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
const monthContainer = document.getElementById('js-month-chart');
const weekdayContainer = document.getElementById('js-weekday-chart');
const hourContainer = document.getElementById('js-hour-chart');
const LANGUAGE_CHART_HEIGHT = 300;
const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
if (firstDayOfWeek === 0) {
return weekDays;
......
......@@ -4,6 +4,8 @@ require 'digest/md5'
require 'uri'
module ApplicationHelper
include StartupCssHelper
# See https://docs.gitlab.com/ee/development/ee_features.html#code-in-app-views
# rubocop: disable CodeReuse/ActiveRecord
def render_if_exists(partial, locals = {})
......@@ -235,10 +237,6 @@ module ApplicationHelper
"#{request.path}?#{options.compact.to_param}"
end
def use_startup_css?
(Feature.enabled?(:startup_css) || params[:startup_css] == 'true' || cookies['startup_css'] == 'true') && !Rails.env.test?
end
def stylesheet_link_tag_defer(path)
if use_startup_css?
stylesheet_link_tag(path, media: "print", crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil)
......
# frozen_string_literal: true
module StartupCssHelper
def use_startup_css?
(Feature.enabled?(:startup_css) || params[:startup_css] == 'true' || cookies['startup_css'] == 'true') && !Rails.env.test?
end
end
......@@ -3,5 +3,8 @@
= javascript_tag nonce: true do
:plain
document.querySelectorAll('link[media="print"]').forEach(linkTag => {
linkTag.addEventListener('load', function() {this.media='all'}, {once: true});
linkTag.setAttribute('data-startupcss', 'loading');
const startupLinkLoadedEvent = new CustomEvent('CSSStartupLinkLoaded');
linkTag.addEventListener('load',function(){this.media='all';this.setAttribute('data-startupcss', 'loaded');document.dispatchEvent(startupLinkLoadedEvent);},{once: true});
})
- return unless use_startup_css?
---
title: Initialise charts when container display property is set
merge_request: 40787
author:
type: fixed
......@@ -4,6 +4,7 @@
module Gitlab
module GonHelper
include StartupCssHelper
include WebpackHelper
def add_gon_variables
......@@ -48,6 +49,9 @@ module Gitlab
push_frontend_feature_flag(:snippets_edit_vue, default_enabled: false)
push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)
# Startup CSS feature is a special one as it can be enabled by means of cookies and params
gon.push({ features: { 'startupCss' => use_startup_css? } }, true)
end
# Exposes the state of a feature flag to the frontend code.
......
import {
handleLoadedEvents,
waitForCSSLoaded,
} 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', () => {
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'));
await events;
expect(docDispatch).toHaveBeenCalledTimes(3);
expect(docDispatch.mock.calls[2][0].type).toBe('CSSLoaded');
expect(docRemoveListener).toHaveBeenCalledTimes(1);
expect(mock).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 = `
<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');
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 () => {
const events = waitForCSSLoaded(mock);
const fixtures = `
<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');
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);
});
});
});
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