Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 13:34:06 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 13:34:06 +0300
commit859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch)
treed7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /spec/frontend/snippets
parent446d496a6d000c73a304be52587cd9bbc7493136 (diff)
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'spec/frontend/snippets')
-rw-r--r--spec/frontend/snippets/components/edit_spec.js585
-rw-r--r--spec/frontend/snippets/components/embed_dropdown_spec.js4
-rw-r--r--spec/frontend/snippets/components/show_spec.js9
-rw-r--r--spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js8
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js8
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js8
-rw-r--r--spec/frontend/snippets/components/snippet_title_spec.js2
-rw-r--r--spec/frontend/snippets/test_utils.js55
-rw-r--r--spec/frontend/snippets/utils/error_spec.js16
10 files changed, 433 insertions, 264 deletions
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index b818f98efb1..2b6d3ca8c2a 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -1,138 +1,120 @@
-import VueApollo, { ApolloMutation } from 'vue-apollo';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import waitForPromises from 'helpers/wait_for_promises';
+import { merge } from 'lodash';
+import { nextTick } from 'vue';
+import VueApollo, { ApolloMutation } from 'vue-apollo';
+import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
+import waitForPromises from 'helpers/wait_for_promises';
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
+import CaptchaModal from '~/captcha/captcha_modal.vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
import * as urlUtils from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
+import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
-import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
-import TitleField from '~/vue_shared/components/form/title.vue';
-import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import {
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
-import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql';
-import { testEntries } from '../test_utils';
+import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql';
+import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
+import TitleField from '~/vue_shared/components/form/title.vue';
+import { testEntries, createGQLSnippetsQueryResponse, createGQLSnippet } from '../test_utils';
jest.mock('~/flash');
const TEST_UPLOADED_FILES = ['foo/bar.txt', 'alpha/beta.js'];
-const TEST_API_ERROR = 'Ufff';
-const TEST_MUTATION_ERROR = 'Bummer';
-
+const TEST_API_ERROR = new Error('TEST_API_ERROR');
+const TEST_MUTATION_ERROR = 'Test mutation error';
+const TEST_CAPTCHA_RESPONSE = 'i-got-a-captcha';
+const TEST_CAPTCHA_SITE_KEY = 'abc123';
const TEST_ACTIONS = {
- NO_CONTENT: {
- ...testEntries.created.diff,
- content: '',
- },
- NO_PATH: {
- ...testEntries.created.diff,
- filePath: '',
- },
- VALID: {
- ...testEntries.created.diff,
- },
+ NO_CONTENT: merge({}, testEntries.created.diff, { content: '' }),
+ NO_PATH: merge({}, testEntries.created.diff, { filePath: '' }),
+ VALID: merge({}, testEntries.created.diff),
};
-
const TEST_WEB_URL = '/snippets/7';
+const TEST_SNIPPET_GID = 'gid://gitlab/PersonalSnippet/42';
+
+const createSnippet = () =>
+ merge(createGQLSnippet(), {
+ webUrl: TEST_WEB_URL,
+ visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
+ });
+
+const createQueryResponse = (obj = {}) =>
+ createGQLSnippetsQueryResponse([merge(createSnippet(), obj)]);
+
+const createMutationResponse = (key, obj = {}) => ({
+ data: {
+ [key]: merge(
+ {
+ errors: [],
+ snippet: {
+ __typename: 'Snippet',
+ webUrl: TEST_WEB_URL,
+ },
+ spamLogId: null,
+ needsCaptchaResponse: false,
+ captchaSiteKey: null,
+ },
+ obj,
+ ),
+ },
+});
+
+const createMutationResponseWithErrors = (key) =>
+ createMutationResponse(key, { errors: [TEST_MUTATION_ERROR] });
+
+const createMutationResponseWithRecaptcha = (key) =>
+ createMutationResponse(key, {
+ errors: ['ignored captcha error message'],
+ needsCaptchaResponse: true,
+ captchaSiteKey: TEST_CAPTCHA_SITE_KEY,
+ });
-const createTestSnippet = () => ({
- webUrl: TEST_WEB_URL,
- id: 7,
- title: 'Snippet Title',
- description: 'Lorem ipsum snippet desc',
- visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
+const getApiData = ({
+ id,
+ title = '',
+ description = '',
+ visibilityLevel = SNIPPET_VISIBILITY_PRIVATE,
+} = {}) => ({
+ id,
+ title,
+ description,
+ visibilityLevel,
+ blobActions: [],
});
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
describe('Snippet Edit app', () => {
+ useFakeDate();
+
let wrapper;
- let fakeApollo;
+ let getSpy;
+
+ // Mutate spy receives a "key" so that we can:
+ // - Use the same spy whether we are creating or updating.
+ // - Build the correct response object
+ // - Assert which mutation was sent
+ let mutateSpy;
+
const relativeUrlRoot = '/foo/';
const originalRelativeUrlRoot = gon.relative_url_root;
- const GetSnippetQuerySpy = jest.fn().mockResolvedValue({
- data: { snippets: { nodes: [createTestSnippet()] } },
- });
- const mutationTypes = {
- RESOLVE: jest.fn().mockResolvedValue({
- data: {
- updateSnippet: {
- errors: [],
- snippet: createTestSnippet(),
- },
- },
- }),
- RESOLVE_WITH_ERRORS: jest.fn().mockResolvedValue({
- data: {
- updateSnippet: {
- errors: [TEST_MUTATION_ERROR],
- snippet: createTestSnippet(),
- },
- createSnippet: {
- errors: [TEST_MUTATION_ERROR],
- snippet: null,
- },
- },
- }),
- REJECT: jest.fn().mockRejectedValue(TEST_API_ERROR),
- };
-
- function createComponent({
- props = {},
- loading = false,
- mutationRes = mutationTypes.RESOLVE,
- selectedLevel = SNIPPET_VISIBILITY_PRIVATE,
- withApollo = false,
- } = {}) {
- let componentData = {
- mocks: {
- $apollo: {
- queries: {
- snippet: { loading },
- },
- mutate: mutationRes,
- },
- },
- };
-
- if (withApollo) {
- const localVue = createLocalVue();
- localVue.use(VueApollo);
-
- const requestHandlers = [[GetSnippetQuery, GetSnippetQuerySpy]];
- fakeApollo = createMockApollo(requestHandlers);
- componentData = {
- localVue,
- apolloProvider: fakeApollo,
- };
- }
+ beforeEach(() => {
+ getSpy = jest.fn().mockResolvedValue(createQueryResponse());
- wrapper = shallowMount(SnippetEditApp, {
- ...componentData,
- stubs: {
- ApolloMutation,
- FormFooterActions,
- },
- provide: {
- selectedLevel,
- },
- propsData: {
- snippetGid: 'gid://gitlab/PersonalSnippet/42',
- markdownPreviewPath: 'http://preview.foo.bar',
- markdownDocsPath: 'http://docs.foo.bar',
- ...props,
- },
- });
- }
+ // See `mutateSpy` declaration comment for why we send a key
+ mutateSpy = jest.fn().mockImplementation((key) => Promise.resolve(createMutationResponse(key)));
- beforeEach(() => {
gon.relative_url_root = relativeUrlRoot;
jest.spyOn(urlUtils, 'redirectTo').mockImplementation();
});
@@ -144,10 +126,10 @@ describe('Snippet Edit app', () => {
});
const findBlobActions = () => wrapper.find(SnippetBlobActionsEdit);
+ const findCaptchaModal = () => wrapper.find(CaptchaModal);
const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]');
const findCancelButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]');
const hasDisabledSubmit = () => Boolean(findSubmitButton().attributes('disabled'));
-
const clickSubmitBtn = () => wrapper.find('[data-testid="snippet-edit-form"]').trigger('submit');
const triggerBlobActions = (actions) => findBlobActions().vm.$emit('actions', actions);
const setUploadFilesHtml = (paths) => {
@@ -155,53 +137,92 @@ describe('Snippet Edit app', () => {
.map((path) => `<input name="files[]" value="${path}">`)
.join('');
};
- const getApiData = ({
- id,
- title = '',
- description = '',
- visibilityLevel = SNIPPET_VISIBILITY_PRIVATE,
- } = {}) => ({
- id,
- title,
- description,
- visibilityLevel,
- blobActions: [],
- });
+ const setTitle = (val) => wrapper.find(TitleField).vm.$emit('input', val);
+ const setDescription = (val) => wrapper.find(SnippetDescriptionEdit).vm.$emit('input', val);
- // Ideally we wouldn't call this method directly, but we don't have a way to trigger
- // apollo responses yet.
- const loadSnippet = (...nodes) => {
- if (nodes.length) {
- wrapper.setData({
- snippet: nodes[0],
- newSnippet: false,
- });
- } else {
- wrapper.setData({
- newSnippet: true,
- });
+ const createComponent = ({ props = {}, selectedLevel = SNIPPET_VISIBILITY_PRIVATE } = {}) => {
+ if (wrapper) {
+ throw new Error('wrapper already created');
}
+
+ const requestHandlers = [
+ [GetSnippetQuery, getSpy],
+ // See `mutateSpy` declaration comment for why we send a key
+ [UpdateSnippetMutation, (...args) => mutateSpy('updateSnippet', ...args)],
+ [CreateSnippetMutation, (...args) => mutateSpy('createSnippet', ...args)],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMount(SnippetEditApp, {
+ apolloProvider,
+ localVue,
+ stubs: {
+ ApolloMutation,
+ FormFooterActions,
+ CaptchaModal: stubComponent(CaptchaModal),
+ },
+ provide: {
+ selectedLevel,
+ },
+ propsData: {
+ snippetGid: TEST_SNIPPET_GID,
+ markdownPreviewPath: 'http://preview.foo.bar',
+ markdownDocsPath: 'http://docs.foo.bar',
+ ...props,
+ },
+ });
};
- describe('rendering', () => {
- it('renders loader while the query is in flight', () => {
- createComponent({ loading: true });
+ // Creates comopnent and waits for gql load
+ const createComponentAndLoad = async (...args) => {
+ createComponent(...args);
+
+ await waitForPromises();
+ };
+
+ // Creates loaded component and submits form
+ const createComponentAndSubmit = async (...args) => {
+ await createComponentAndLoad(...args);
+
+ clickSubmitBtn();
+
+ await waitForPromises();
+ };
+
+ describe('when loading', () => {
+ it('renders loader', () => {
+ createComponent();
+
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
+ });
- it.each([[{}], [{ snippetGid: '' }]])(
- 'should render all required components with %s',
- (props) => {
- createComponent(props);
+ describe.each`
+ snippetGid | expectedQueries
+ ${TEST_SNIPPET_GID} | ${[[{ ids: [TEST_SNIPPET_GID] }]]}
+ ${''} | ${[]}
+ `('when loaded with snippetGid=$snippetGid', ({ snippetGid, expectedQueries }) => {
+ beforeEach(() => createComponentAndLoad({ props: { snippetGid } }));
- expect(wrapper.find(TitleField).exists()).toBe(true);
- expect(wrapper.find(SnippetDescriptionEdit).exists()).toBe(true);
- expect(wrapper.find(SnippetVisibilityEdit).exists()).toBe(true);
- expect(wrapper.find(FormFooterActions).exists()).toBe(true);
- expect(findBlobActions().exists()).toBe(true);
- },
- );
+ it(`queries with ${JSON.stringify(expectedQueries)}`, () => {
+ expect(getSpy.mock.calls).toEqual(expectedQueries);
+ });
+ it('should render components', () => {
+ expect(wrapper.find(CaptchaModal).exists()).toBe(true);
+ expect(wrapper.find(TitleField).exists()).toBe(true);
+ expect(wrapper.find(SnippetDescriptionEdit).exists()).toBe(true);
+ expect(wrapper.find(SnippetVisibilityEdit).exists()).toBe(true);
+ expect(wrapper.find(FormFooterActions).exists()).toBe(true);
+ expect(findBlobActions().exists()).toBe(true);
+ });
+
+ it('should hide loader', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+ });
+
+ describe('default', () => {
it.each`
title | actions | shouldDisable
${''} | ${[]} | ${true}
@@ -211,163 +232,241 @@ describe('Snippet Edit app', () => {
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_CONTENT]} | ${true}
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${false}
`(
- 'should handle submit disable (title=$title, actions=$actions, shouldDisable=$shouldDisable)',
+ 'should handle submit disable (title="$title", actions="$actions", shouldDisable="$shouldDisable")',
async ({ title, actions, shouldDisable }) => {
- createComponent();
+ getSpy.mockResolvedValue(createQueryResponse({ title }));
+
+ await createComponentAndLoad();
- loadSnippet({ title });
triggerBlobActions(actions);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(hasDisabledSubmit()).toBe(shouldDisable);
},
);
it.each`
- projectPath | snippetArg | expectation
- ${''} | ${[]} | ${urlUtils.joinPaths('/', relativeUrlRoot, '-', 'snippets')}
- ${'project/path'} | ${[]} | ${urlUtils.joinPaths('/', relativeUrlRoot, 'project/path/-', 'snippets')}
- ${''} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
- ${'project/path'} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
+ projectPath | snippetGid | expectation
+ ${''} | ${''} | ${urlUtils.joinPaths('/', relativeUrlRoot, '-', 'snippets')}
+ ${'project/path'} | ${''} | ${urlUtils.joinPaths('/', relativeUrlRoot, 'project/path/-', 'snippets')}
+ ${''} | ${TEST_SNIPPET_GID} | ${TEST_WEB_URL}
+ ${'project/path'} | ${TEST_SNIPPET_GID} | ${TEST_WEB_URL}
`(
- 'should set cancel href when (projectPath=$projectPath, snippet=$snippetArg)',
- async ({ projectPath, snippetArg, expectation }) => {
- createComponent({
- props: { projectPath },
+ 'should set cancel href (projectPath="$projectPath", snippetGid="$snippetGid")',
+ async ({ projectPath, snippetGid, expectation }) => {
+ await createComponentAndLoad({
+ props: {
+ projectPath,
+ snippetGid,
+ },
});
- loadSnippet(...snippetArg);
-
- await wrapper.vm.$nextTick();
-
expect(findCancelButton().attributes('href')).toBe(expectation);
},
);
- });
-
- describe('functionality', () => {
- it('does not fetch snippet when create a new snippet', async () => {
- createComponent({ props: { snippetGid: '' }, withApollo: true });
-
- jest.runOnlyPendingTimers();
- await wrapper.vm.$nextTick();
- expect(GetSnippetQuerySpy).not.toHaveBeenCalled();
- });
+ it.each([SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC])(
+ 'marks %s visibility by default',
+ async (visibility) => {
+ createComponent({
+ props: { snippetGid: '' },
+ selectedLevel: visibility,
+ });
- describe('default visibility', () => {
- it.each([SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC])(
- 'marks %s visibility by default',
- async (visibility) => {
- createComponent({
- props: { snippetGid: '' },
- selectedLevel: visibility,
- });
- expect(wrapper.vm.snippet.visibilityLevel).toEqual(visibility);
- },
- );
- });
+ expect(wrapper.find(SnippetVisibilityEdit).props('value')).toBe(visibility);
+ },
+ );
describe('form submission handling', () => {
it.each`
- snippetArg | projectPath | uploadedFiles | input | mutation
- ${[]} | ${'project/path'} | ${[]} | ${{ ...getApiData(), projectPath: 'project/path', uploadedFiles: [] }} | ${CreateSnippetMutation}
- ${[]} | ${''} | ${[]} | ${{ ...getApiData(), projectPath: '', uploadedFiles: [] }} | ${CreateSnippetMutation}
- ${[]} | ${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData(), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }} | ${CreateSnippetMutation}
- ${[createTestSnippet()]} | ${'project/path'} | ${[]} | ${getApiData(createTestSnippet())} | ${UpdateSnippetMutation}
- ${[createTestSnippet()]} | ${''} | ${[]} | ${getApiData(createTestSnippet())} | ${UpdateSnippetMutation}
+ snippetGid | projectPath | uploadedFiles | input | mutationType
+ ${''} | ${'project/path'} | ${[]} | ${{ ...getApiData(), projectPath: 'project/path', uploadedFiles: [] }} | ${'createSnippet'}
+ ${''} | ${''} | ${[]} | ${{ ...getApiData(), projectPath: '', uploadedFiles: [] }} | ${'createSnippet'}
+ ${''} | ${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData(), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }} | ${'createSnippet'}
+ ${TEST_SNIPPET_GID} | ${'project/path'} | ${[]} | ${getApiData(createSnippet())} | ${'updateSnippet'}
+ ${TEST_SNIPPET_GID} | ${''} | ${[]} | ${getApiData(createSnippet())} | ${'updateSnippet'}
`(
- 'should submit mutation with (snippet=$snippetArg, projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
- async ({ snippetArg, projectPath, uploadedFiles, mutation, input }) => {
- createComponent({
+ 'should submit mutation $mutationType (snippetGid=$snippetGid, projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
+ async ({ snippetGid, projectPath, uploadedFiles, mutationType, input }) => {
+ await createComponentAndLoad({
props: {
+ snippetGid,
projectPath,
},
});
- loadSnippet(...snippetArg);
+
setUploadFilesHtml(uploadedFiles);
- await wrapper.vm.$nextTick();
+ await nextTick();
clickSubmitBtn();
- expect(mutationTypes.RESOLVE).toHaveBeenCalledWith({
- mutation,
- variables: {
- input,
- },
+ expect(mutateSpy).toHaveBeenCalledTimes(1);
+ expect(mutateSpy).toHaveBeenCalledWith(mutationType, {
+ input,
});
},
);
it('should redirect to snippet view on successful mutation', async () => {
- createComponent();
- loadSnippet(createTestSnippet());
-
- clickSubmitBtn();
-
- await waitForPromises();
+ await createComponentAndSubmit();
expect(urlUtils.redirectTo).toHaveBeenCalledWith(TEST_WEB_URL);
});
it.each`
- snippetArg | projectPath | mutationRes | expectMessage
- ${[]} | ${'project/path'} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
- ${[]} | ${''} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
- ${[]} | ${''} | ${mutationTypes.REJECT} | ${`Can't create snippet: ${TEST_API_ERROR}`}
- ${[createTestSnippet()]} | ${'project/path'} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
- ${[createTestSnippet()]} | ${''} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
+ snippetGid | projectPath | mutationRes | expectMessage
+ ${''} | ${'project/path'} | ${createMutationResponseWithErrors('createSnippet')} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
+ ${''} | ${''} | ${createMutationResponseWithErrors('createSnippet')} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
+ ${TEST_SNIPPET_GID} | ${'project/path'} | ${createMutationResponseWithErrors('updateSnippet')} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
+ ${TEST_SNIPPET_GID} | ${''} | ${createMutationResponseWithErrors('updateSnippet')} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
`(
- 'should flash error with (snippet=$snippetArg, projectPath=$projectPath)',
- async ({ snippetArg, projectPath, mutationRes, expectMessage }) => {
- createComponent({
+ 'should flash error with (snippet=$snippetGid, projectPath=$projectPath)',
+ async ({ snippetGid, projectPath, mutationRes, expectMessage }) => {
+ mutateSpy.mockResolvedValue(mutationRes);
+
+ await createComponentAndSubmit({
props: {
projectPath,
+ snippetGid,
},
- mutationRes,
});
- loadSnippet(...snippetArg);
-
- clickSubmitBtn();
-
- await waitForPromises();
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
expect(Flash).toHaveBeenCalledWith(expectMessage);
},
);
- });
- describe('on before unload', () => {
- it.each`
- condition | expectPrevented | action
- ${'there are no actions'} | ${false} | ${() => triggerBlobActions([])}
- ${'there are actions'} | ${true} | ${() => triggerBlobActions([testEntries.updated.diff])}
- ${'the snippet is being saved'} | ${false} | ${() => clickSubmitBtn()}
- `(
- 'handles before unload prevent when $condition (expectPrevented=$expectPrevented)',
- ({ expectPrevented, action }) => {
- createComponent();
- loadSnippet();
+ describe('with apollo network error', () => {
+ beforeEach(async () => {
+ jest.spyOn(console, 'error').mockImplementation();
+ mutateSpy.mockRejectedValue(TEST_API_ERROR);
- action();
+ await createComponentAndSubmit();
+ });
- const event = new Event('beforeunload');
- const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
+ it('should not redirect', () => {
+ expect(urlUtils.redirectTo).not.toHaveBeenCalled();
+ });
- window.dispatchEvent(event);
+ it('should flash', () => {
+ // Apollo automatically wraps the resolver's error in a NetworkError
+ expect(Flash).toHaveBeenCalledWith(
+ `Can't update snippet: Network error: ${TEST_API_ERROR.message}`,
+ );
+ });
- if (expectPrevented) {
- expect(returnValueSetter).toHaveBeenCalledWith(
- 'Are you sure you want to lose unsaved changes?',
- );
- } else {
- expect(returnValueSetter).not.toHaveBeenCalled();
- }
- },
- );
+ it('should console error', () => {
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalledTimes(1);
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalledWith(
+ '[gitlab] unexpected error while updating snippet',
+ expect.objectContaining({ message: `Network error: ${TEST_API_ERROR.message}` }),
+ );
+ });
+ });
+
+ describe('when needsCaptchaResponse is true', () => {
+ let modal;
+
+ beforeEach(async () => {
+ mutateSpy
+ .mockResolvedValueOnce(createMutationResponseWithRecaptcha('updateSnippet'))
+ .mockResolvedValueOnce(createMutationResponseWithErrors('updateSnippet'));
+
+ await createComponentAndSubmit();
+
+ modal = findCaptchaModal();
+
+ mutateSpy.mockClear();
+ });
+
+ it('should display captcha modal', () => {
+ expect(urlUtils.redirectTo).not.toHaveBeenCalled();
+ expect(modal.props()).toEqual({
+ needsCaptchaResponse: true,
+ captchaSiteKey: TEST_CAPTCHA_SITE_KEY,
+ });
+ });
+
+ describe.each`
+ response | expectedCalls
+ ${null} | ${[]}
+ ${TEST_CAPTCHA_RESPONSE} | ${[['updateSnippet', { input: { ...getApiData(createSnippet()), captchaResponse: TEST_CAPTCHA_RESPONSE } }]]}
+ `('when captcha response is $response', ({ response, expectedCalls }) => {
+ beforeEach(async () => {
+ modal.vm.$emit('receivedCaptchaResponse', response);
+
+ await nextTick();
+ });
+
+ it('sets needsCaptchaResponse to false', () => {
+ expect(modal.props('needsCaptchaResponse')).toEqual(false);
+ });
+
+ it(`expected to call times = ${expectedCalls.length}`, () => {
+ expect(mutateSpy.mock.calls).toEqual(expectedCalls);
+ });
+ });
+ });
});
});
+
+ describe('on before unload', () => {
+ it.each([
+ ['there are no actions', false, () => triggerBlobActions([])],
+ ['there is an empty action', false, () => triggerBlobActions([testEntries.empty.diff])],
+ ['there are actions', true, () => triggerBlobActions([testEntries.updated.diff])],
+ [
+ 'the title is set',
+ true,
+ () => {
+ triggerBlobActions([testEntries.empty.diff]);
+ setTitle('test');
+ },
+ ],
+ [
+ 'the description is set',
+ true,
+ () => {
+ triggerBlobActions([testEntries.empty.diff]);
+ setDescription('test');
+ },
+ ],
+ [
+ 'the snippet is being saved',
+ false,
+ () => {
+ triggerBlobActions([testEntries.updated.diff]);
+ clickSubmitBtn();
+ },
+ ],
+ ])(
+ 'handles before unload prevent when %s (expectPrevented=%s)',
+ async (_, expectPrevented, action) => {
+ await createComponentAndLoad({
+ props: {
+ snippetGid: '',
+ },
+ });
+
+ action();
+
+ const event = new Event('beforeunload');
+ const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
+
+ window.dispatchEvent(event);
+
+ if (expectPrevented) {
+ expect(returnValueSetter).toHaveBeenCalledWith(
+ 'Are you sure you want to lose unsaved changes?',
+ );
+ } else {
+ expect(returnValueSetter).not.toHaveBeenCalled();
+ }
+ },
+ );
+ });
});
diff --git a/spec/frontend/snippets/components/embed_dropdown_spec.js b/spec/frontend/snippets/components/embed_dropdown_spec.js
index f1eb7d43409..389b1c618a3 100644
--- a/spec/frontend/snippets/components/embed_dropdown_spec.js
+++ b/spec/frontend/snippets/components/embed_dropdown_spec.js
@@ -1,6 +1,6 @@
-import { escape as esc } from 'lodash';
-import { mount } from '@vue/test-utils';
import { GlFormInputGroup } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { escape as esc } from 'lodash';
import { TEST_HOST } from 'helpers/test_constants';
import EmbedDropdown from '~/snippets/components/embed_dropdown.vue';
diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index b5ab7def753..e6162c6aad2 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -1,18 +1,17 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { shallowMount } from '@vue/test-utils';
-import SnippetApp from '~/snippets/components/show.vue';
+import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import EmbedDropdown from '~/snippets/components/embed_dropdown.vue';
+import SnippetApp from '~/snippets/components/show.vue';
+import SnippetBlob from '~/snippets/components/snippet_blob_view.vue';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
import SnippetTitle from '~/snippets/components/snippet_title.vue';
-import SnippetBlob from '~/snippets/components/snippet_blob_view.vue';
-import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
-
import {
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
+import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
describe('Snippet view app', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
index 08056e788de..2693b26aeae 100644
--- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
@@ -1,5 +1,5 @@
-import { times } from 'lodash';
import { shallowMount } from '@vue/test-utils';
+import { times } from 'lodash';
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import {
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index 9d0311fd682..a7ab205ca7b 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -1,14 +1,14 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
-import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
+import waitForPromises from 'helpers/wait_for_promises';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
+import EditorLite from '~/vue_shared/components/editor_lite.vue';
jest.mock('~/flash');
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index 1ccecd7b5ba..b92c1907980 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -1,5 +1,5 @@
-import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import {
Blob as BlobMock,
SimpleViewerMock,
@@ -7,16 +7,16 @@ import {
RichBlobContentMock,
SimpleBlobContentMock,
} from 'jest/blob/components/mock_data';
-import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
-import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContent from '~/blob/components/blob_content.vue';
+import BlobHeader from '~/blob/components/blob_header.vue';
import {
BLOB_RENDER_EVENT_LOAD,
BLOB_RENDER_EVENT_SHOW_SOURCE,
BLOB_RENDER_ERRORS,
} from '~/blob/components/constants';
-import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
+import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
+import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
describe('Blob Embeddable', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 93a66db32c6..585614a6b79 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -1,11 +1,11 @@
-import { ApolloMutation } from 'vue-apollo';
import { GlButton, GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
+import { ApolloMutation } from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
-import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
-import SnippetHeader from '~/snippets/components/snippet_header.vue';
+import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
+import SnippetHeader from '~/snippets/components/snippet_header.vue';
+import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
describe('Snippet header component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_title_spec.js b/spec/frontend/snippets/components/snippet_title_spec.js
index f201cfb19b7..48fb51ce703 100644
--- a/spec/frontend/snippets/components/snippet_title_spec.js
+++ b/spec/frontend/snippets/components/snippet_title_spec.js
@@ -1,7 +1,7 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import SnippetTitle from '~/snippets/components/snippet_title.vue';
import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
+import SnippetTitle from '~/snippets/components/snippet_title.vue';
describe('Snippet header component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/test_utils.js b/spec/frontend/snippets/test_utils.js
index 86262723157..8ba5a2fe5dc 100644
--- a/spec/frontend/snippets/test_utils.js
+++ b/spec/frontend/snippets/test_utils.js
@@ -1,3 +1,4 @@
+import { TEST_HOST } from 'helpers/test_constants';
import {
SNIPPET_BLOB_ACTION_CREATE,
SNIPPET_BLOB_ACTION_UPDATE,
@@ -8,6 +9,51 @@ import {
const CONTENT_1 = 'Lorem ipsum dolar\nSit amit\n\nGoodbye!\n';
const CONTENT_2 = 'Lorem ipsum dolar sit amit.\n\nGoodbye!\n';
+export const createGQLSnippet = () => ({
+ __typename: 'Snippet',
+ id: 7,
+ title: 'Snippet Title',
+ description: 'Lorem ipsum snippet desc',
+ descriptionHtml: '<p>Lorem ipsum snippet desc</p>',
+ createdAt: new Date(Date.now() - 1e6),
+ updatedAt: new Date(Date.now() - 1e3),
+ httpUrlToRepo: `${TEST_HOST}/repo`,
+ sshUrlToRepo: 'ssh://ssh.test/repo',
+ blobs: [],
+ userPermissions: {
+ __typename: 'SnippetPermissions',
+ adminSnippet: true,
+ updateSnippet: true,
+ },
+ project: {
+ __typename: 'Project',
+ fullPath: 'group/project',
+ webUrl: `${TEST_HOST}/group/project`,
+ },
+ author: {
+ __typename: 'User',
+ id: 1,
+ avatarUrl: `${TEST_HOST}/avatar.png`,
+ name: 'root',
+ username: 'root',
+ webUrl: `${TEST_HOST}/root`,
+ status: {
+ __typename: 'UserStatus',
+ emoji: '',
+ message: '',
+ },
+ },
+});
+
+export const createGQLSnippetsQueryResponse = (snippets) => ({
+ data: {
+ snippets: {
+ __typename: 'SnippetConnection',
+ nodes: snippets,
+ },
+ },
+});
+
export const testEntries = {
created: {
id: 'blob_1',
@@ -56,6 +102,15 @@ export const testEntries = {
content: CONTENT_2,
},
},
+ empty: {
+ id: 'empty',
+ diff: {
+ action: SNIPPET_BLOB_ACTION_CREATE,
+ filePath: '',
+ previousPath: '',
+ content: '',
+ },
+ },
};
export const createBlobFromTestEntry = ({ diff, origContent }, isOrig = false) => ({
diff --git a/spec/frontend/snippets/utils/error_spec.js b/spec/frontend/snippets/utils/error_spec.js
new file mode 100644
index 00000000000..385554568db
--- /dev/null
+++ b/spec/frontend/snippets/utils/error_spec.js
@@ -0,0 +1,16 @@
+import { getErrorMessage, UNEXPECTED_ERROR } from '~/snippets/utils/error';
+
+describe('~/snippets/utils/error', () => {
+ describe('getErrorMessage', () => {
+ it.each`
+ input | output
+ ${null} | ${UNEXPECTED_ERROR}
+ ${'message'} | ${'message'}
+ ${new Error('test message')} | ${'test message'}
+ ${{ networkError: 'Network error: test message' }} | ${'Network error: test message'}
+ ${{}} | ${UNEXPECTED_ERROR}
+ `('with $input, should return "$output"', ({ input, output }) => {
+ expect(getErrorMessage(input)).toBe(output);
+ });
+ });
+});