gitlab-ce/spec/frontend/glql/components/common/facade_spec.js

250 lines
8.6 KiB
JavaScript

import { GlAlert, GlButton, GlSkeletonLoader, GlIntersectionObserver } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
import { stubCrypto } from 'helpers/crypto';
import GlqlFacade from '~/glql/components/common/facade.vue';
import GlqlActions from '~/glql/components/common/actions.vue';
import { executeAndPresentQuery, presentPreview } from '~/glql/core';
import Counter from '~/glql/utils/counter';
import { MOCK_ISSUES } from '../../mock_data';
jest.mock('~/glql/core');
describe('GlqlFacade', () => {
let wrapper;
const mockQueryKey = 'glql_key';
const { bindInternalEventDocument } = useMockInternalEventsTracking();
const createComponent = async (props = {}, glFeatures = {}) => {
wrapper = mountExtended(GlqlFacade, {
propsData: {
query: 'assignee = "foo"',
...props,
},
provide: {
glFeatures,
queryKey: mockQueryKey,
},
});
await nextTick();
await waitForPromises();
};
const triggerIntersectionObserver = async () => {
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();
await waitForPromises();
};
beforeEach(stubCrypto);
describe('when glqlLoadOnClick feature flag is enabled', () => {
beforeEach(async () => {
await createComponent({}, { glqlLoadOnClick: true });
});
it('renders the query in a code block', () => {
expect(wrapper.find('code').text()).toBe('assignee = "foo"');
});
it('renders the Load embedded view button', () => {
expect(wrapper.findComponent(GlButton).text()).toEqual('Load embedded view');
});
});
it('shows skeleton loader when loading', async () => {
presentPreview.mockResolvedValue({ component: { render: (h) => h(GlSkeletonLoader) } });
executeAndPresentQuery.mockImplementation(() => new Promise(() => {})); // Never resolves
await createComponent();
await triggerIntersectionObserver();
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
describe('when the query is successful', () => {
const MockComponent = { render: (h) => h('div') };
beforeEach(async () => {
presentPreview.mockResolvedValue({ component: { render: (h) => h(GlSkeletonLoader) } });
executeAndPresentQuery.mockResolvedValue({
component: MockComponent,
data: { count: 2, ...MOCK_ISSUES },
});
await createComponent();
await triggerIntersectionObserver();
});
it('renders presenter component after successful query execution', () => {
expect(wrapper.findComponent(MockComponent).exists()).toBe(true);
});
it('renders actions', () => {
expect(wrapper.findComponent(GlqlActions).props()).toEqual({
modalTitle: 'Embedded list view',
showCopyContents: true,
});
});
it('renders a footer text', () => {
expect(wrapper.text()).toContain('Embedded view powered by GLQL');
});
it('shows a "No data" message if the list of items provided is empty', async () => {
presentPreview.mockResolvedValue({ component: { render: (h) => h(GlSkeletonLoader) } });
executeAndPresentQuery.mockResolvedValue({
component: MockComponent,
data: { count: 0, nodes: [] },
});
await createComponent();
await triggerIntersectionObserver();
expect(wrapper.text()).toContain('No data found for this query');
});
it('tracks GLQL render event', () => {
const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
expect(trackEventSpy).toHaveBeenCalledWith(
'render_glql_block',
{ label: expect.any(String) },
undefined,
);
});
it('reloads the query when reload event is emitted on event hub', async () => {
jest.spyOn(wrapper.vm, 'reloadGlqlBlock');
wrapper.findComponent(GlqlActions).vm.$emit('reload');
await nextTick();
expect(wrapper.vm.reloadGlqlBlock).toHaveBeenCalled();
});
});
describe('when the query results in a timeout (503) error', () => {
beforeEach(async () => {
presentPreview.mockResolvedValue({ component: { render: (h) => h(GlSkeletonLoader) } });
executeAndPresentQuery.mockRejectedValue({ networkError: { statusCode: 503 } });
await createComponent();
await triggerIntersectionObserver();
});
it('displays timeout error alert', () => {
const alert = wrapper.findComponent(GlAlert);
expect(alert.exists()).toBe(true);
expect(alert.props('variant')).toBe('warning');
expect(alert.text()).toContain(
'Embedded view timed out. Add more filters to reduce the number of results.',
);
expect(alert.props('primaryButtonText')).toBe('Retry');
});
it('retries query execution when primary action of timeout error alert is triggered', async () => {
presentPreview.mockClear();
executeAndPresentQuery.mockClear();
executeAndPresentQuery.mockResolvedValue({ component: { render: (h) => h('div') } });
const alert = wrapper.findComponent(GlAlert);
alert.vm.$emit('primaryAction');
await nextTick();
expect(executeAndPresentQuery).toHaveBeenCalledWith('assignee = "foo"', 'glql_key');
});
});
describe('when the query results in a forbidden (403) error', () => {
beforeEach(async () => {
presentPreview.mockResolvedValue({ component: { render: (h) => h(GlSkeletonLoader) } });
executeAndPresentQuery.mockRejectedValue({ networkError: { statusCode: 403 } });
await createComponent();
await triggerIntersectionObserver();
});
it('displays forbidden error alert', () => {
const alert = wrapper.findComponent(GlAlert);
expect(alert.exists()).toBe(true);
expect(alert.props('variant')).toBe('danger');
expect(alert.text()).toContain('Embedded view timed out. Try again later.');
});
});
describe('when the query results in a syntax error', () => {
beforeEach(async () => {
presentPreview.mockRejectedValue(new Error('Syntax error: Unexpected `=`'));
executeAndPresentQuery.mockRejectedValue(new Error('Syntax error: Unexpected `=`'));
await createComponent();
await triggerIntersectionObserver();
});
it('displays error alert on query failure, formatted by marked', () => {
const alert = wrapper.findComponent(GlAlert);
expect(alert.exists()).toBe(true);
expect(alert.props('variant')).toBe('warning');
expect(alert.find('ul li').html()).toMatchInlineSnapshot(`
<li>
Syntax error: Unexpected
<code>
=
</code>
</li>
`);
});
it('dismisses alert when dismiss event is emitted', async () => {
let alert = wrapper.findComponent(GlAlert);
expect(alert.exists()).toBe(true);
alert.vm.$emit('dismiss');
await nextTick();
alert = wrapper.findComponent(GlAlert);
expect(alert.exists()).toBe(false);
});
});
describe('when number of GLQL blocks on page exceeds the limit', () => {
beforeEach(async () => {
presentPreview.mockResolvedValue({ component: { render: (h) => h(GlSkeletonLoader) } });
executeAndPresentQuery.mockResolvedValue({ render: (h) => h('div') });
// Simulate exceeding the limit
jest.spyOn(Counter.prototype, 'increment').mockImplementation(() => {
throw new Error('Counter exceeded max value');
});
await createComponent();
await triggerIntersectionObserver();
});
it('displays limit error alert after exceeding embedded view block limit', () => {
const alert = wrapper.findComponent(GlAlert);
expect(alert.exists()).toBe(true);
expect(alert.props('variant')).toBe('warning');
expect(alert.text()).toContain(
'Only 20 embedded views can be automatically displayed on a page. Click the button below to manually display this block.',
);
expect(alert.props('primaryButtonText')).toBe('Display block');
});
it('retries query execution when primary action of limit error alert is triggered', async () => {
presentPreview.mockClear();
presentPreview.mockResolvedValue({ component: { render: (h) => h(GlSkeletonLoader) } });
executeAndPresentQuery.mockClear();
executeAndPresentQuery.mockResolvedValue({ component: { render: (h) => h('div') } });
const alert = wrapper.findComponent(GlAlert);
alert.vm.$emit('primaryAction');
await nextTick();
expect(executeAndPresentQuery).toHaveBeenCalledWith('assignee = "foo"', 'glql_key');
});
});
});