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 Vue from 'vue';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { waitForCSSLoaded } from '../../../../helpers/startup_css_helper';
import { __ } from '~/locale'; import { __ } from '~/locale';
import CodeCoverage from '../components/code_coverage.vue'; import CodeCoverage from '../components/code_coverage.vue';
import SeriesDataMixin from './series_data_mixin'; import SeriesDataMixin from './series_data_mixin';
document.addEventListener('DOMContentLoaded', () => { waitForCSSLoaded(() => {
const languagesContainer = document.getElementById('js-languages-chart'); const languagesContainer = document.getElementById('js-languages-chart');
const codeCoverageContainer = document.getElementById('js-code-coverage-chart'); const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
const monthContainer = document.getElementById('js-month-chart'); const monthContainer = document.getElementById('js-month-chart');
const weekdayContainer = document.getElementById('js-weekday-chart'); const weekdayContainer = document.getElementById('js-weekday-chart');
const hourContainer = document.getElementById('js-hour-chart'); const hourContainer = document.getElementById('js-hour-chart');
const LANGUAGE_CHART_HEIGHT = 300; const LANGUAGE_CHART_HEIGHT = 300;
const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => { const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
if (firstDayOfWeek === 0) { if (firstDayOfWeek === 0) {
return weekDays; return weekDays;
......
...@@ -4,6 +4,8 @@ require 'digest/md5' ...@@ -4,6 +4,8 @@ require 'digest/md5'
require 'uri' require 'uri'
module ApplicationHelper module ApplicationHelper
include StartupCssHelper
# See https://docs.gitlab.com/ee/development/ee_features.html#code-in-app-views # See https://docs.gitlab.com/ee/development/ee_features.html#code-in-app-views
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def render_if_exists(partial, locals = {}) def render_if_exists(partial, locals = {})
...@@ -235,10 +237,6 @@ module ApplicationHelper ...@@ -235,10 +237,6 @@ module ApplicationHelper
"#{request.path}?#{options.compact.to_param}" "#{request.path}?#{options.compact.to_param}"
end 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) def stylesheet_link_tag_defer(path)
if use_startup_css? if use_startup_css?
stylesheet_link_tag(path, media: "print", crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil) 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 @@ ...@@ -3,5 +3,8 @@
= javascript_tag nonce: true do = javascript_tag nonce: true do
:plain :plain
document.querySelectorAll('link[media="print"]').forEach(linkTag => { 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 @@ ...@@ -4,6 +4,7 @@
module Gitlab module Gitlab
module GonHelper module GonHelper
include StartupCssHelper
include WebpackHelper include WebpackHelper
def add_gon_variables def add_gon_variables
...@@ -48,6 +49,9 @@ module Gitlab ...@@ -48,6 +49,9 @@ module Gitlab
push_frontend_feature_flag(:snippets_edit_vue, default_enabled: false) push_frontend_feature_flag(:snippets_edit_vue, default_enabled: false)
push_frontend_feature_flag(:webperf_experiment, default_enabled: false) push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
push_frontend_feature_flag(:snippets_binary_blob, 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 end
# Exposes the state of a feature flag to the frontend code. # 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