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:
Diffstat (limited to 'spec/frontend/snippets/components/edit_spec.js')
-rw-r--r--spec/frontend/snippets/components/edit_spec.js539
1 files changed, 215 insertions, 324 deletions
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index d2265dfd506..980855a0615 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -1,134 +1,157 @@
-import { shallowMount } from '@vue/test-utils';
-import Flash from '~/flash';
-
+import { ApolloMutation } from 'vue-apollo';
import { GlLoadingIcon } from '@gitlab/ui';
-import { redirectTo } from '~/lib/utils/url_utility';
-
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import * as urlUtils from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
-import SnippetBlobEdit from '~/snippets/components/snippet_blob_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_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '~/snippets/constants';
-
+import { SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants';
import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql';
-
-import waitForPromises from 'helpers/wait_for_promises';
-import { ApolloMutation } from 'vue-apollo';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- redirectTo: jest.fn().mockName('redirectTo'),
-}));
+import { testEntries } from '../test_utils';
jest.mock('~/flash');
-let flashSpy;
-
-const rawProjectPathMock = '/project/path';
-const newlyEditedSnippetUrl = 'http://foo.bar';
-const apiError = { message: 'Ufff' };
-const mutationError = 'Bummer';
-
-const attachedFilePath1 = 'foo/bar';
-const attachedFilePath2 = 'alpha/beta';
-
-const actionWithContent = {
- content: 'Foo Bar',
-};
-const actionWithoutContent = {
- content: '',
-};
+const TEST_UPLOADED_FILES = ['foo/bar.txt', 'alpha/beta.js'];
+const TEST_API_ERROR = 'Ufff';
+const TEST_MUTATION_ERROR = 'Bummer';
-const defaultProps = {
- snippetGid: 'gid://gitlab/PersonalSnippet/42',
- markdownPreviewPath: 'http://preview.foo.bar',
- markdownDocsPath: 'http://docs.foo.bar',
-};
-const defaultData = {
- blobsActions: {
- ...actionWithContent,
- action: '',
+const TEST_ACTIONS = {
+ NO_CONTENT: {
+ ...testEntries.created.diff,
+ content: '',
+ },
+ NO_PATH: {
+ ...testEntries.created.diff,
+ filePath: '',
+ },
+ VALID: {
+ ...testEntries.created.diff,
},
};
+const TEST_WEB_URL = '/snippets/7';
+
+const createTestSnippet = () => ({
+ webUrl: TEST_WEB_URL,
+ id: 7,
+ title: 'Snippet Title',
+ description: 'Lorem ipsum snippet desc',
+ visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
+});
+
describe('Snippet Edit app', () => {
let wrapper;
- const resolveMutate = jest.fn().mockResolvedValue({
- data: {
- updateSnippet: {
- errors: [],
- snippet: {
- webUrl: newlyEditedSnippetUrl,
+ const mutationTypes = {
+ RESOLVE: jest.fn().mockResolvedValue({
+ data: {
+ updateSnippet: {
+ errors: [],
+ snippet: createTestSnippet(),
},
},
- },
- });
-
- const resolveMutateWithErrors = jest.fn().mockResolvedValue({
- data: {
- updateSnippet: {
- errors: [mutationError],
- snippet: {
- webUrl: newlyEditedSnippetUrl,
+ }),
+ RESOLVE_WITH_ERRORS: jest.fn().mockResolvedValue({
+ data: {
+ updateSnippet: {
+ errors: [TEST_MUTATION_ERROR],
+ snippet: createTestSnippet(),
+ },
+ createSnippet: {
+ errors: [TEST_MUTATION_ERROR],
+ snippet: null,
},
},
- createSnippet: {
- errors: [mutationError],
- snippet: null,
- },
- },
- });
-
- const rejectMutation = jest.fn().mockRejectedValue(apiError);
-
- const mutationTypes = {
- RESOLVE: resolveMutate,
- RESOLVE_WITH_ERRORS: resolveMutateWithErrors,
- REJECT: rejectMutation,
+ }),
+ REJECT: jest.fn().mockRejectedValue(TEST_API_ERROR),
};
function createComponent({
- props = defaultProps,
- data = {},
+ props = {},
loading = false,
mutationRes = mutationTypes.RESOLVE,
} = {}) {
- const $apollo = {
- queries: {
- snippet: {
- loading,
- },
- },
- mutate: mutationRes,
- };
+ if (wrapper) {
+ throw new Error('wrapper already exists');
+ }
wrapper = shallowMount(SnippetEditApp, {
- mocks: { $apollo },
+ mocks: {
+ $apollo: {
+ queries: {
+ snippet: { loading },
+ },
+ mutate: mutationRes,
+ },
+ },
stubs: {
- FormFooterActions,
ApolloMutation,
+ FormFooterActions,
},
propsData: {
+ snippetGid: 'gid://gitlab/PersonalSnippet/42',
+ markdownPreviewPath: 'http://preview.foo.bar',
+ markdownDocsPath: 'http://docs.foo.bar',
...props,
},
- data() {
- return data;
- },
});
-
- flashSpy = jest.spyOn(wrapper.vm, 'flashAPIFailure');
}
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'redirectTo').mockImplementation();
+ });
+
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
+ const findBlobActions = () => wrapper.find(SnippetBlobActionsEdit);
const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]');
- const findCancellButton = () => wrapper.find('[data-testid="snippet-cancel-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 => {
+ wrapper.vm.$el.innerHTML = paths.map(path => `<input name="files[]" value="${path}">`).join('');
+ };
+ const getApiData = ({
+ id,
+ title = '',
+ description = '',
+ visibilityLevel = SNIPPET_VISIBILITY_PRIVATE,
+ } = {}) => ({
+ id,
+ title,
+ description,
+ visibilityLevel,
+ blobActions: [],
+ });
+
+ // Ideally we wouldn't call this method directly, but we don't have a way to trigger
+ // apollo responses yet.
+ const loadSnippet = (...edges) => {
+ if (edges.length) {
+ wrapper.setData({
+ snippet: edges[0],
+ });
+ }
+
+ wrapper.vm.onSnippetFetch({
+ data: {
+ snippets: {
+ edges,
+ },
+ },
+ });
+ };
describe('rendering', () => {
it('renders loader while the query is in flight', () => {
@@ -136,295 +159,163 @@ describe('Snippet Edit app', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- it('renders all required components', () => {
- createComponent();
-
- expect(wrapper.contains(TitleField)).toBe(true);
- expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true);
- expect(wrapper.contains(SnippetBlobEdit)).toBe(true);
- expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true);
- expect(wrapper.contains(FormFooterActions)).toBe(true);
- });
-
- it('does not fail if there is no snippet yet (new snippet creation)', () => {
- const snippetGid = '';
- createComponent({
- props: {
- ...defaultProps,
- snippetGid,
- },
- });
-
- expect(wrapper.props('snippetGid')).toBe(snippetGid);
- });
+ it.each([[{}], [{ snippetGid: '' }]])(
+ 'should render all required components with %s',
+ props => {
+ createComponent(props);
- it.each`
- title | blobsActions | expectation
- ${''} | ${{}} | ${true}
- ${''} | ${{ actionWithContent }} | ${true}
- ${''} | ${{ actionWithoutContent }} | ${true}
- ${'foo'} | ${{}} | ${true}
- ${'foo'} | ${{ actionWithoutContent }} | ${true}
- ${'foo'} | ${{ actionWithoutContent, actionWithContent }} | ${true}
- ${'foo'} | ${{ actionWithContent }} | ${false}
- `(
- 'disables submit button unless both title and content for all blobs are present',
- ({ title, blobsActions, expectation }) => {
- createComponent({
- data: {
- snippet: { title },
- blobsActions,
- },
- });
- const isBtnDisabled = Boolean(findSubmitButton().attributes('disabled'));
- expect(isBtnDisabled).toBe(expectation);
+ expect(wrapper.contains(TitleField)).toBe(true);
+ expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true);
+ expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true);
+ expect(wrapper.contains(FormFooterActions)).toBe(true);
+ expect(findBlobActions().exists()).toBe(true);
},
);
it.each`
- isNew | status | expectation
- ${true} | ${`new`} | ${`/snippets`}
- ${false} | ${`existing`} | ${newlyEditedSnippetUrl}
- `('sets correct href for the cancel button on a $status snippet', ({ isNew, expectation }) => {
- createComponent({
- data: {
- snippet: { webUrl: newlyEditedSnippetUrl },
- newSnippet: isNew,
- },
- });
+ title | actions | shouldDisable
+ ${''} | ${[]} | ${true}
+ ${''} | ${[TEST_ACTIONS.VALID]} | ${true}
+ ${'foo'} | ${[]} | ${false}
+ ${'foo'} | ${[TEST_ACTIONS.VALID]} | ${false}
+ ${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_CONTENT]} | ${true}
+ ${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${true}
+ `(
+ 'should handle submit disable (title=$title, actions=$actions, shouldDisable=$shouldDisable)',
+ async ({ title, actions, shouldDisable }) => {
+ createComponent();
- expect(findCancellButton().attributes('href')).toBe(expectation);
- });
- });
+ loadSnippet({ title });
+ triggerBlobActions(actions);
- describe('functionality', () => {
- describe('form submission handling', () => {
- it('does not submit unchanged blobs', () => {
- const foo = {
- action: '',
- };
- const bar = {
- action: 'update',
- };
- createComponent({
- data: {
- blobsActions: {
- foo,
- bar,
- },
- },
- });
- clickSubmitBtn();
+ await wrapper.vm.$nextTick();
- return waitForPromises().then(() => {
- expect(resolveMutate).toHaveBeenCalledWith(
- expect.objectContaining({ variables: { input: { files: [bar] } } }),
- );
- });
- });
+ expect(hasDisabledSubmit()).toBe(shouldDisable);
+ },
+ );
- it.each`
- newSnippet | projectPath | mutation | mutationName
- ${true} | ${rawProjectPathMock} | ${CreateSnippetMutation} | ${'CreateSnippetMutation with projectPath'}
- ${true} | ${''} | ${CreateSnippetMutation} | ${'CreateSnippetMutation without projectPath'}
- ${false} | ${rawProjectPathMock} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation with projectPath'}
- ${false} | ${''} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation without projectPath'}
- `('should submit $mutationName correctly', ({ newSnippet, projectPath, mutation }) => {
+ it.each`
+ projectPath | snippetArg | expectation
+ ${''} | ${[]} | ${'/-/snippets'}
+ ${'project/path'} | ${[]} | ${'/project/path/-/snippets'}
+ ${''} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
+ ${'project/path'} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
+ `(
+ 'should set cancel href when (projectPath=$projectPath, snippet=$snippetArg)',
+ async ({ projectPath, snippetArg, expectation }) => {
createComponent({
- data: {
- newSnippet,
- ...defaultData,
- },
- props: {
- ...defaultProps,
- projectPath,
- },
+ props: { projectPath },
});
- const mutationPayload = {
- mutation,
- variables: {
- input: newSnippet ? expect.objectContaining({ projectPath }) : expect.any(Object),
- },
- };
-
- clickSubmitBtn();
-
- expect(resolveMutate).toHaveBeenCalledWith(mutationPayload);
- });
+ loadSnippet(...snippetArg);
- it('redirects to snippet view on successful mutation', () => {
- createComponent();
- clickSubmitBtn();
+ await wrapper.vm.$nextTick();
- return waitForPromises().then(() => {
- expect(redirectTo).toHaveBeenCalledWith(newlyEditedSnippetUrl);
- });
- });
+ expect(findCancelButton().attributes('href')).toBe(expectation);
+ },
+ );
+ });
+ describe('functionality', () => {
+ describe('form submission handling', () => {
it.each`
- newSnippet | projectPath | mutationName
- ${true} | ${rawProjectPathMock} | ${'CreateSnippetMutation with projectPath'}
- ${true} | ${''} | ${'CreateSnippetMutation without projectPath'}
- ${false} | ${rawProjectPathMock} | ${'UpdateSnippetMutation with projectPath'}
- ${false} | ${''} | ${'UpdateSnippetMutation without projectPath'}
+ 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}
`(
- 'does not redirect to snippet view if the seemingly successful' +
- ' $mutationName response contains errors',
- ({ newSnippet, projectPath }) => {
+ 'should submit mutation with (snippet=$snippetArg, projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
+ async ({ snippetArg, projectPath, uploadedFiles, mutation, input }) => {
createComponent({
- data: {
- newSnippet,
- },
props: {
- ...defaultProps,
projectPath,
},
- mutationRes: mutationTypes.RESOLVE_WITH_ERRORS,
});
+ loadSnippet(...snippetArg);
+ setUploadFilesHtml(uploadedFiles);
+
+ await wrapper.vm.$nextTick();
clickSubmitBtn();
- return waitForPromises().then(() => {
- expect(redirectTo).not.toHaveBeenCalled();
- expect(flashSpy).toHaveBeenCalledWith(mutationError);
+ expect(mutationTypes.RESOLVE).toHaveBeenCalledWith({
+ mutation,
+ variables: {
+ input,
+ },
});
},
);
- it('flashes an error if mutation failed', () => {
- createComponent({
- mutationRes: mutationTypes.REJECT,
- });
+ it('should redirect to snippet view on successful mutation', async () => {
+ createComponent();
+ loadSnippet(createTestSnippet());
clickSubmitBtn();
- return waitForPromises().then(() => {
- expect(redirectTo).not.toHaveBeenCalled();
- expect(flashSpy).toHaveBeenCalledWith(apiError);
- });
+ await waitForPromises();
+
+ expect(urlUtils.redirectTo).toHaveBeenCalledWith(TEST_WEB_URL);
});
it.each`
- isNew | status | expectation
- ${true} | ${`new`} | ${SNIPPET_CREATE_MUTATION_ERROR.replace('%{err}', '')}
- ${false} | ${`existing`} | ${SNIPPET_UPDATE_MUTATION_ERROR.replace('%{err}', '')}
+ 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}`}
`(
- `renders the correct error message if mutation fails for $status snippet`,
- ({ isNew, expectation }) => {
+ 'should flash error with (snippet=$snippetArg, projectPath=$projectPath)',
+ async ({ snippetArg, projectPath, mutationRes, expectMessage }) => {
createComponent({
- data: {
- newSnippet: isNew,
+ props: {
+ projectPath,
},
- mutationRes: mutationTypes.REJECT,
+ mutationRes,
});
+ loadSnippet(...snippetArg);
clickSubmitBtn();
- return waitForPromises().then(() => {
- expect(Flash).toHaveBeenCalledWith(expect.stringContaining(expectation));
- });
+ await waitForPromises();
+
+ expect(urlUtils.redirectTo).not.toHaveBeenCalled();
+ expect(Flash).toHaveBeenCalledWith(expectMessage);
},
);
});
- describe('correctly includes attached files into the mutation', () => {
- const createMutationPayload = expectation => {
- return expect.objectContaining({
- variables: {
- input: expect.objectContaining({ uploadedFiles: expectation }),
- },
- });
- };
-
- const updateMutationPayload = () => {
- return expect.objectContaining({
- variables: {
- input: expect.not.objectContaining({ uploadedFiles: expect.anything() }),
- },
- });
- };
-
- it.each`
- paths | expectation
- ${[attachedFilePath1]} | ${[attachedFilePath1]}
- ${[attachedFilePath1, attachedFilePath2]} | ${[attachedFilePath1, attachedFilePath2]}
- ${[]} | ${[]}
- `(`correctly sends paths for $paths.length files`, ({ paths, expectation }) => {
- createComponent({
- data: {
- newSnippet: true,
- },
- });
-
- const fixtures = paths.map(path => {
- return path ? `<input name="files[]" value="${path}">` : undefined;
- });
- wrapper.vm.$el.innerHTML += fixtures.join('');
-
- clickSubmitBtn();
-
- expect(resolveMutate).toHaveBeenCalledWith(createMutationPayload(expectation));
- });
-
- it(`neither fails nor sends 'uploadedFiles' to update mutation`, () => {
- createComponent();
-
- clickSubmitBtn();
- expect(resolveMutate).toHaveBeenCalledWith(updateMutationPayload());
- });
- });
-
describe('on before unload', () => {
- let event;
- let returnValueSetter;
-
- const bootstrap = data => {
- createComponent({
- data,
- });
-
- event = new Event('beforeunload');
- returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
- };
-
- it('does not prevent page navigation if there are no blobs', () => {
- bootstrap();
- window.dispatchEvent(event);
-
- expect(returnValueSetter).not.toHaveBeenCalled();
- });
-
- it('does not prevent page navigation if there are no changes to the blobs content', () => {
- bootstrap({
- blobsActions: {
- foo: {
- ...actionWithContent,
- action: '',
- },
- },
- });
- window.dispatchEvent(event);
+ 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();
- expect(returnValueSetter).not.toHaveBeenCalled();
- });
+ action();
- it('prevents page navigation if there are some changes in the snippet content', () => {
- bootstrap({
- blobsActions: {
- foo: {
- ...actionWithContent,
- action: 'update',
- },
- },
- });
+ const event = new Event('beforeunload');
+ const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
- window.dispatchEvent(event);
+ window.dispatchEvent(event);
- expect(returnValueSetter).toHaveBeenCalledWith(
- 'Are you sure you want to lose unsaved changes?',
- );
- });
+ if (expectPrevented) {
+ expect(returnValueSetter).toHaveBeenCalledWith(
+ 'Are you sure you want to lose unsaved changes?',
+ );
+ } else {
+ expect(returnValueSetter).not.toHaveBeenCalled();
+ }
+ },
+ );
});
});
});