Commit 095ac79b authored by Michael Lunøe's avatar Michael Lunøe Committed by Simon Knox

Feat(Ensure Data): add component

This component makes it easy to ensure that data
is present before rendering an application,
displaying an error message and, if necessary,
log it to Sentry when the data parsing fails
parent 0e8dfef6
import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg';
import { GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { __ } from '~/locale';
const ERROR_FETCHING_DATA_HEADER = __('Could not get the data properly');
const ERROR_FETCHING_DATA_DESCRIPTION = __(
'Please try and refresh the page. If the problem persists please contact support.',
);
/**
* This function takes a Component and extends it with data from the `parseData` function.
* The data will be made available through `props` and `proivde`.
* If the `parseData` throws, the `GlEmptyState` will be returned.
* @param {Component} Component a component to render
* @param {Object} options
* @param {Function} options.parseData a function to parse `data`
* @param {Object} options.data an object to pass to `parseData`
* @param {Boolean} options.shouldLog to tell whether to log any thrown error by `parseData` to Sentry
* @param {Object} options.props to override passed `props` data
* @param {Object} options.provide to override passed `provide` data
* @param {*} ...options the remaining options will be passed as properties to `createElement`
* @return {Component} a Vue component to render, either the GlEmptyState or the extended Component
*/
export default function ensureData(Component, options = {}) {
const { parseData, data, shouldLog = false, props, provide, ...rest } = options;
try {
const parsedData = parseData(data);
return {
provide: { ...parsedData, ...provide },
render(createElement) {
return createElement(Component, {
props: { ...parsedData, ...props },
...rest,
});
},
};
} catch (error) {
if (shouldLog) {
Sentry.captureException(error);
}
return {
functional: true,
render(createElement) {
return createElement(GlEmptyState, {
props: {
title: ERROR_FETCHING_DATA_HEADER,
description: ERROR_FETCHING_DATA_DESCRIPTION,
svgPath: `data:image/svg+xml;utf8,${encodeURIComponent(emptySvg)}`,
},
});
},
};
}
}
...@@ -8668,6 +8668,9 @@ msgstr "" ...@@ -8668,6 +8668,9 @@ msgstr ""
msgid "Could not find iteration" msgid "Could not find iteration"
msgstr "" msgstr ""
msgid "Could not get the data properly"
msgstr ""
msgid "Could not load the user chart. Please refresh the page to try again." msgid "Could not load the user chart. Please refresh the page to try again."
msgstr "" msgstr ""
...@@ -23005,6 +23008,9 @@ msgstr "" ...@@ -23005,6 +23008,9 @@ msgstr ""
msgid "Please try again" msgid "Please try again"
msgstr "" msgstr ""
msgid "Please try and refresh the page. If the problem persists please contact support."
msgstr ""
msgid "Please type %{phrase_code} to proceed or close this modal to cancel." msgid "Please type %{phrase_code} to proceed or close this modal to cancel."
msgstr "" msgstr ""
......
import { GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import ensureData from '~/ensure_data';
const mockData = { message: 'Hello there' };
const defaultOptions = {
parseData: () => mockData,
data: mockData,
};
const MockChildComponent = {
inject: ['message'],
render(createElement) {
return createElement('h1', this.message);
},
};
const MockParentComponent = {
components: {
MockChildComponent,
},
props: {
message: {
type: String,
required: true,
},
otherProp: {
type: Boolean,
default: false,
required: false,
},
},
render(createElement) {
return createElement('div', [this.message, createElement(MockChildComponent)]);
},
};
describe('EnsureData', () => {
let wrapper;
function findEmptyState() {
return wrapper.findComponent(GlEmptyState);
}
function findChild() {
return wrapper.findComponent(MockChildComponent);
}
function findParent() {
return wrapper.findComponent(MockParentComponent);
}
function createComponent(options = defaultOptions) {
return mount(ensureData(MockParentComponent, options));
}
beforeEach(() => {
Sentry.captureException = jest.fn();
});
afterEach(() => {
wrapper.destroy();
Sentry.captureException.mockClear();
});
describe('when parseData throws', () => {
it('should render GlEmptyState', () => {
wrapper = createComponent({
parseData: () => {
throw new Error();
},
});
expect(findParent().exists()).toBe(false);
expect(findChild().exists()).toBe(false);
expect(findEmptyState().exists()).toBe(true);
});
it('should not log to Sentry when shouldLog=false (default)', () => {
wrapper = createComponent({
parseData: () => {
throw new Error();
},
});
expect(Sentry.captureException).not.toHaveBeenCalled();
});
it('should log to Sentry when shouldLog=true', () => {
const error = new Error('Error!');
wrapper = createComponent({
parseData: () => {
throw error;
},
shouldLog: true,
});
expect(Sentry.captureException).toHaveBeenCalledWith(error);
});
});
describe('when parseData succeeds', () => {
it('should render MockParentComponent and MockChildComponent', () => {
wrapper = createComponent();
expect(findEmptyState().exists()).toBe(false);
expect(findParent().exists()).toBe(true);
expect(findChild().exists()).toBe(true);
});
it('enables user to provide data to child components', () => {
wrapper = createComponent();
const childComponent = findChild();
expect(childComponent.text()).toBe(mockData.message);
});
it('enables user to override provide data', () => {
const message = 'Another message';
wrapper = createComponent({ ...defaultOptions, provide: { message } });
const childComponent = findChild();
expect(childComponent.text()).toBe(message);
});
it('enables user to pass props to parent component', () => {
wrapper = createComponent();
expect(findParent().props()).toMatchObject(mockData);
});
it('enables user to override props data', () => {
const props = { message: 'Another message', otherProp: true };
wrapper = createComponent({ ...defaultOptions, props });
expect(findParent().props()).toMatchObject(props);
});
it('should not log to Sentry when shouldLog=true', () => {
wrapper = createComponent({ ...defaultOptions, shouldLog: true });
expect(Sentry.captureException).not.toHaveBeenCalled();
});
});
});
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