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')
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap32
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap2
-rw-r--r--spec/frontend/snippets/components/edit_spec.js539
-rw-r--r--spec/frontend/snippets/components/show_spec.js59
-rw-r--r--spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js301
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js227
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js82
-rw-r--r--spec/frontend/snippets/components/snippet_description_edit_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_description_view_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js112
-rw-r--r--spec/frontend/snippets/components/snippet_title_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js4
-rw-r--r--spec/frontend/snippets/test_utils.js76
-rw-r--r--spec/frontend/snippets/utils/blob_spec.js63
14 files changed, 977 insertions, 528 deletions
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
index 959bc24eef6..1cf1ee74ddf 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
@@ -1,25 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Snippet Blob Edit component rendering matches the snapshot 1`] = `
+exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = `
<div
- class="form-group file-editor"
+ class="file-holder snippet"
>
- <label>
- File
- </label>
+ <blob-header-edit-stub
+ candelete="true"
+ data-qa-selector="file_name_field"
+ id="blob_local_7_file_path"
+ value="foo/bar/test.md"
+ />
- <div
- class="file-holder snippet"
- >
- <blob-header-edit-stub
- data-qa-selector="file_name_field"
- value="lorem.txt"
- />
-
- <blob-content-edit-stub
- filename="lorem.txt"
- value="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
- />
- </div>
+ <blob-content-edit-stub
+ fileglobalid="blob_local_7"
+ filename="foo/bar/test.md"
+ value="Lorem ipsum dolar sit amet,
+consectetur adipiscing elit."
+ />
</div>
`;
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index 297ad16b681..6020d595e3f 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -60,7 +60,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
<a
aria-label="Leave zen mode"
- class="zen-control zen-control-leave js-zen-leave gl-text-gray-700"
+ class="zen-control zen-control-leave js-zen-leave gl-text-gray-500"
href="#"
>
<icon-stub
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();
+ }
+ },
+ );
});
});
});
diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index b5446e70028..8cccbb83d54 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -1,19 +1,27 @@
+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 BlobEmbeddable from '~/blob/components/blob_embeddable.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 { GlLoadingIcon } from '@gitlab/ui';
-import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
+import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
-import { shallowMount } from '@vue/test-utils';
-import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
+import {
+ SNIPPET_VISIBILITY_INTERNAL,
+ SNIPPET_VISIBILITY_PRIVATE,
+ SNIPPET_VISIBILITY_PUBLIC,
+} from '~/snippets/constants';
describe('Snippet view app', () => {
let wrapper;
const defaultProps = {
snippetGid: 'gid://gitlab/PersonalSnippet/42',
};
+ const webUrl = 'http://foo.bar';
+ const dummyHTTPUrl = webUrl;
+ const dummySSHUrl = 'ssh://foo.bar';
function createComponent({ props = defaultProps, data = {}, loading = false } = {}) {
const $apollo = {
@@ -72,4 +80,47 @@ describe('Snippet view app', () => {
expect(blobs.at(0).props('blob')).toEqual(Blob);
expect(blobs.at(1).props('blob')).toEqual(BinaryBlob);
});
+
+ describe('Embed dropdown rendering', () => {
+ it.each`
+ visibilityLevel | condition | isRendered
+ ${SNIPPET_VISIBILITY_INTERNAL} | ${'not render'} | ${false}
+ ${SNIPPET_VISIBILITY_PRIVATE} | ${'not render'} | ${false}
+ ${'foo'} | ${'not render'} | ${false}
+ ${SNIPPET_VISIBILITY_PUBLIC} | ${'render'} | ${true}
+ `('does $condition blob-embeddable by default', ({ visibilityLevel, isRendered }) => {
+ createComponent({
+ data: {
+ snippet: {
+ visibilityLevel,
+ webUrl,
+ },
+ },
+ });
+ expect(wrapper.contains(BlobEmbeddable)).toBe(isRendered);
+ });
+ });
+
+ describe('Clone button rendering', () => {
+ it.each`
+ httpUrlToRepo | sshUrlToRepo | shouldRender | isRendered
+ ${null} | ${null} | ${'Should not'} | ${false}
+ ${null} | ${dummySSHUrl} | ${'Should'} | ${true}
+ ${dummyHTTPUrl} | ${null} | ${'Should'} | ${true}
+ ${dummyHTTPUrl} | ${dummySSHUrl} | ${'Should'} | ${true}
+ `(
+ '$shouldRender render "Clone" button when `httpUrlToRepo` is $httpUrlToRepo and `sshUrlToRepo` is $sshUrlToRepo',
+ ({ httpUrlToRepo, sshUrlToRepo, isRendered }) => {
+ createComponent({
+ data: {
+ snippet: {
+ sshUrlToRepo,
+ httpUrlToRepo,
+ },
+ },
+ });
+ expect(wrapper.contains(CloneDropdownButton)).toBe(isRendered);
+ },
+ );
+ });
});
diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
new file mode 100644
index 00000000000..8b2051008d7
--- /dev/null
+++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
@@ -0,0 +1,301 @@
+import { times } from 'lodash';
+import { shallowMount } from '@vue/test-utils';
+import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
+import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
+import {
+ SNIPPET_MAX_BLOBS,
+ SNIPPET_BLOB_ACTION_CREATE,
+ SNIPPET_BLOB_ACTION_MOVE,
+} from '~/snippets/constants';
+import { testEntries, createBlobFromTestEntry } from '../test_utils';
+
+const TEST_BLOBS = [
+ createBlobFromTestEntry(testEntries.updated),
+ createBlobFromTestEntry(testEntries.deleted),
+];
+
+const TEST_BLOBS_UNLOADED = TEST_BLOBS.map(blob => ({ ...blob, content: '', isLoaded: false }));
+
+describe('snippets/components/snippet_blob_actions_edit', () => {
+ let wrapper;
+
+ const createComponent = (props = {}, snippetMultipleFiles = true) => {
+ wrapper = shallowMount(SnippetBlobActionsEdit, {
+ propsData: {
+ initBlobs: TEST_BLOBS,
+ ...props,
+ },
+ provide: {
+ glFeatures: {
+ snippetMultipleFiles,
+ },
+ },
+ });
+ };
+
+ const findLabel = () => wrapper.find('label');
+ const findBlobEdits = () => wrapper.findAll(SnippetBlobEdit);
+ const findBlobsData = () =>
+ findBlobEdits().wrappers.map(x => ({
+ blob: x.props('blob'),
+ classes: x.classes(),
+ }));
+ const findFirstBlobEdit = () => findBlobEdits().at(0);
+ const findAddButton = () => wrapper.find('[data-testid="add_button"]');
+ const getLastActions = () => {
+ const events = wrapper.emitted().actions;
+
+ return events[events.length - 1]?.[0];
+ };
+ const buildBlobsDataExpectation = blobs =>
+ blobs.map((blob, index) => ({
+ blob: {
+ ...blob,
+ id: expect.stringMatching('blob_local_'),
+ },
+ classes: index > 0 ? ['gl-mt-3'] : [],
+ }));
+ const triggerBlobDelete = idx =>
+ findBlobEdits()
+ .at(idx)
+ .vm.$emit('delete');
+ const triggerBlobUpdate = (idx, props) =>
+ findBlobEdits()
+ .at(idx)
+ .vm.$emit('blob-updated', props);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe.each`
+ featureFlag | label | showDelete | showAdd
+ ${true} | ${'Files'} | ${true} | ${true}
+ ${false} | ${'File'} | ${false} | ${false}
+ `('with feature flag = $featureFlag', ({ featureFlag, label, showDelete, showAdd }) => {
+ beforeEach(() => {
+ createComponent({}, featureFlag);
+ });
+
+ it('renders label', () => {
+ expect(findLabel().text()).toBe(label);
+ });
+
+ it(`renders delete button (show=${showDelete})`, () => {
+ expect(findFirstBlobEdit().props()).toMatchObject({
+ showDelete,
+ canDelete: true,
+ });
+ });
+
+ it(`renders add button (show=${showAdd})`, () => {
+ expect(findAddButton().exists()).toBe(showAdd);
+ });
+ });
+
+ describe('with default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('emits no actions', () => {
+ expect(getLastActions()).toEqual([]);
+ });
+
+ it('shows blobs', () => {
+ expect(findBlobsData()).toEqual(buildBlobsDataExpectation(TEST_BLOBS_UNLOADED));
+ });
+
+ it('shows add button', () => {
+ const button = findAddButton();
+
+ expect(button.text()).toBe(`Add another file ${TEST_BLOBS.length}/${SNIPPET_MAX_BLOBS}`);
+ expect(button.props('disabled')).toBe(false);
+ });
+
+ describe('when add is clicked', () => {
+ beforeEach(() => {
+ findAddButton().vm.$emit('click');
+ });
+
+ it('adds blob with empty content', () => {
+ expect(findBlobsData()).toEqual(
+ buildBlobsDataExpectation([
+ ...TEST_BLOBS_UNLOADED,
+ {
+ content: '',
+ isLoaded: true,
+ path: '',
+ },
+ ]),
+ );
+ });
+
+ it('emits action', () => {
+ expect(getLastActions()).toEqual([
+ expect.objectContaining({
+ action: SNIPPET_BLOB_ACTION_CREATE,
+ }),
+ ]);
+ });
+ });
+
+ describe('when blob is deleted', () => {
+ beforeEach(() => {
+ triggerBlobDelete(1);
+ });
+
+ it('removes blob', () => {
+ expect(findBlobsData()).toEqual(buildBlobsDataExpectation(TEST_BLOBS_UNLOADED.slice(0, 1)));
+ });
+
+ it('emits action', () => {
+ expect(getLastActions()).toEqual([
+ expect.objectContaining({
+ ...testEntries.deleted.diff,
+ content: '',
+ }),
+ ]);
+ });
+ });
+
+ describe('when blob changes path', () => {
+ beforeEach(() => {
+ triggerBlobUpdate(0, { path: 'new/path' });
+ });
+
+ it('renames blob', () => {
+ expect(findBlobsData()[0]).toMatchObject({
+ blob: {
+ path: 'new/path',
+ },
+ });
+ });
+
+ it('emits action', () => {
+ expect(getLastActions()).toMatchObject([
+ {
+ action: SNIPPET_BLOB_ACTION_MOVE,
+ filePath: 'new/path',
+ previousPath: testEntries.updated.diff.filePath,
+ },
+ ]);
+ });
+ });
+
+ describe('when blob emits new content', () => {
+ const { content } = testEntries.updated.diff;
+ const originalContent = `${content}\noriginal content\n`;
+
+ beforeEach(() => {
+ triggerBlobUpdate(0, { content: originalContent });
+ });
+
+ it('loads new content', () => {
+ expect(findBlobsData()[0]).toMatchObject({
+ blob: {
+ content: originalContent,
+ isLoaded: true,
+ },
+ });
+ });
+
+ it('does not emit an action', () => {
+ expect(getLastActions()).toEqual([]);
+ });
+
+ it('emits an action when content changes again', async () => {
+ triggerBlobUpdate(0, { content });
+
+ await wrapper.vm.$nextTick();
+
+ expect(getLastActions()).toEqual([testEntries.updated.diff]);
+ });
+ });
+ });
+
+ describe('with 1 blob', () => {
+ beforeEach(() => {
+ createComponent({ initBlobs: [createBlobFromTestEntry(testEntries.created)] });
+ });
+
+ it('disables delete button', () => {
+ expect(findBlobEdits()).toHaveLength(1);
+ expect(
+ findBlobEdits()
+ .at(0)
+ .props(),
+ ).toMatchObject({
+ showDelete: true,
+ canDelete: false,
+ });
+ });
+
+ describe(`when added ${SNIPPET_MAX_BLOBS} files`, () => {
+ let addButton;
+
+ beforeEach(() => {
+ addButton = findAddButton();
+
+ times(SNIPPET_MAX_BLOBS - 1, () => addButton.vm.$emit('click'));
+ });
+
+ it('should have blobs', () => {
+ expect(findBlobsData()).toHaveLength(SNIPPET_MAX_BLOBS);
+ });
+
+ it('should disable add button', () => {
+ expect(addButton.props('disabled')).toBe(true);
+ });
+ });
+ });
+
+ describe('with 0 init blob', () => {
+ beforeEach(() => {
+ createComponent({ initBlobs: [] });
+ });
+
+ it('shows 1 blob by default', () => {
+ expect(findBlobsData()).toEqual([
+ expect.objectContaining({
+ blob: {
+ id: expect.stringMatching('blob_local_'),
+ content: '',
+ path: '',
+ isLoaded: true,
+ },
+ }),
+ ]);
+ });
+
+ it('emits create action', () => {
+ expect(getLastActions()).toEqual([
+ {
+ action: SNIPPET_BLOB_ACTION_CREATE,
+ content: '',
+ filePath: '',
+ previousPath: '',
+ },
+ ]);
+ });
+ });
+
+ describe(`with ${SNIPPET_MAX_BLOBS} files`, () => {
+ beforeEach(() => {
+ const initBlobs = Array(SNIPPET_MAX_BLOBS)
+ .fill(1)
+ .map(() => createBlobFromTestEntry(testEntries.created));
+
+ createComponent({ initBlobs });
+ });
+
+ it('should have blobs', () => {
+ expect(findBlobsData()).toHaveLength(SNIPPET_MAX_BLOBS);
+ });
+
+ it('should disable add button', () => {
+ expect(findAddButton().props('disabled')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index 009074b4558..188f9ae5cf1 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -1,165 +1,168 @@
-import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
-import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
-import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
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 BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
+import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
-import waitForPromises from 'helpers/wait_for_promises';
-
-jest.mock('~/blob/utils', () => jest.fn());
-
-jest.mock('~/lib/utils/url_utility', () => ({
- getBaseURL: jest.fn().mockReturnValue('foo/'),
- joinPaths: jest
- .fn()
- .mockName('joinPaths')
- .mockReturnValue('contentApiURL'),
-}));
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
-let flashSpy;
+const TEST_ID = 'blob_local_7';
+const TEST_PATH = 'foo/bar/test.md';
+const TEST_RAW_PATH = '/gitlab/raw/path/to/blob/7';
+const TEST_FULL_PATH = joinPaths(TEST_HOST, TEST_RAW_PATH);
+const TEST_CONTENT = 'Lorem ipsum dolar sit amet,\nconsectetur adipiscing elit.';
+
+const TEST_BLOB = {
+ id: TEST_ID,
+ rawPath: TEST_RAW_PATH,
+ path: TEST_PATH,
+ content: '',
+ isLoaded: false,
+};
+
+const TEST_BLOB_LOADED = {
+ ...TEST_BLOB,
+ content: TEST_CONTENT,
+ isLoaded: true,
+};
describe('Snippet Blob Edit component', () => {
let wrapper;
let axiosMock;
- const contentMock = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
- const pathMock = 'lorem.txt';
- const rawPathMock = 'foo/bar';
- const blob = {
- path: pathMock,
- content: contentMock,
- rawPath: rawPathMock,
- };
- const findComponent = component => wrapper.find(component);
- function createComponent(props = {}, data = { isContentLoading: false }) {
+ const createComponent = (props = {}) => {
wrapper = shallowMount(SnippetBlobEdit, {
propsData: {
+ blob: TEST_BLOB,
...props,
},
- data() {
- return {
- ...data,
- };
- },
});
- flashSpy = jest.spyOn(wrapper.vm, 'flashAPIFailure');
- }
+ };
+
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findHeader = () => wrapper.find(BlobHeaderEdit);
+ const findContent = () => wrapper.find(BlobContentEdit);
+ const getLastUpdatedArgs = () => {
+ const event = wrapper.emitted()['blob-updated'];
+
+ return event?.[event.length - 1][0];
+ };
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
- createComponent();
+ axiosMock.onGet(TEST_FULL_PATH).reply(200, TEST_CONTENT);
});
afterEach(() => {
- axiosMock.restore();
wrapper.destroy();
+ wrapper = null;
+ axiosMock.restore();
});
- describe('rendering', () => {
- it('matches the snapshot', () => {
- createComponent({ blob });
- expect(wrapper.element).toMatchSnapshot();
+ describe('with not loaded blob', () => {
+ beforeEach(async () => {
+ createComponent();
});
- it('renders required components', () => {
- expect(findComponent(BlobHeaderEdit).exists()).toBe(true);
- expect(findComponent(BlobContentEdit).exists()).toBe(true);
+ it('shows blob header', () => {
+ expect(findHeader().props()).toMatchObject({
+ value: TEST_BLOB.path,
+ });
+ expect(findHeader().attributes('id')).toBe(`${TEST_ID}_file_path`);
});
- it('renders loader if existing blob is supplied but no content is fetched yet', () => {
- createComponent({ blob }, { isContentLoading: true });
- expect(wrapper.contains(GlLoadingIcon)).toBe(true);
- expect(findComponent(BlobContentEdit).exists()).toBe(false);
+ it('emits delete when deleted', () => {
+ expect(wrapper.emitted().delete).toBeUndefined();
+
+ findHeader().vm.$emit('delete');
+
+ expect(wrapper.emitted().delete).toHaveLength(1);
});
- it('does not render loader if when blob is not supplied', () => {
- createComponent();
- expect(wrapper.contains(GlLoadingIcon)).toBe(false);
- expect(findComponent(BlobContentEdit).exists()).toBe(true);
+ it('emits update when path changes', () => {
+ const newPath = 'new/path.md';
+
+ findHeader().vm.$emit('input', newPath);
+
+ expect(getLastUpdatedArgs()).toEqual({ path: newPath });
});
- });
- describe('functionality', () => {
- it('does not fail without blob', () => {
- const spy = jest.spyOn(global.console, 'error');
- createComponent({ blob: undefined });
+ it('emits update when content is loaded', async () => {
+ await waitForPromises();
- expect(spy).not.toHaveBeenCalled();
- expect(findComponent(BlobContentEdit).exists()).toBe(true);
+ expect(getLastUpdatedArgs()).toEqual({ content: TEST_CONTENT });
});
+ });
- it.each`
- emitter | prop
- ${BlobHeaderEdit} | ${'filePath'}
- ${BlobContentEdit} | ${'content'}
- `('emits "blob-updated" event when the $prop gets changed', ({ emitter, prop }) => {
- expect(wrapper.emitted('blob-updated')).toBeUndefined();
- const newValue = 'foo.bar';
- findComponent(emitter).vm.$emit('input', newValue);
-
- return nextTick().then(() => {
- expect(wrapper.emitted('blob-updated')[0]).toEqual([
- expect.objectContaining({
- [prop]: newValue,
- }),
- ]);
- });
+ describe('with error', () => {
+ beforeEach(() => {
+ axiosMock.reset();
+ axiosMock.onGet(TEST_FULL_PATH).replyOnce(500);
+ createComponent();
});
- describe('fetching blob content', () => {
- const bootstrapForExistingSnippet = resp => {
- createComponent({
- blob: {
- ...blob,
- content: '',
- },
- });
+ it('should call flash', async () => {
+ await waitForPromises();
- if (resp === 500) {
- axiosMock.onGet('contentApiURL').reply(500);
- } else {
- axiosMock.onGet('contentApiURL').reply(200, contentMock);
- }
- };
+ expect(createFlash).toHaveBeenCalledWith(
+ "Can't fetch content for the blob: Error: Request failed with status code 500",
+ );
+ });
+ });
- const bootstrapForNewSnippet = () => {
- createComponent();
- };
+ describe('with loaded blob', () => {
+ beforeEach(() => {
+ createComponent({ blob: TEST_BLOB_LOADED });
+ });
- it('fetches blob content with the additional query', () => {
- bootstrapForExistingSnippet();
+ it('matches snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
- return waitForPromises().then(() => {
- expect(joinPaths).toHaveBeenCalledWith('foo/', rawPathMock);
- expect(findComponent(BlobHeaderEdit).props('value')).toBe(pathMock);
- expect(findComponent(BlobContentEdit).props('value')).toBe(contentMock);
- });
- });
+ it('does not make API request', () => {
+ expect(axiosMock.history.get).toHaveLength(0);
+ });
+ });
- it('flashes the error message if fetching content fails', () => {
- bootstrapForExistingSnippet(500);
+ describe.each`
+ props | showLoading | showContent
+ ${{ blob: TEST_BLOB, canDelete: true, showDelete: true }} | ${true} | ${false}
+ ${{ blob: TEST_BLOB, canDelete: false, showDelete: false }} | ${true} | ${false}
+ ${{ blob: TEST_BLOB_LOADED }} | ${false} | ${true}
+ `('with $props', ({ props, showLoading, showContent }) => {
+ beforeEach(() => {
+ createComponent(props);
+ });
- return waitForPromises().then(() => {
- expect(flashSpy).toHaveBeenCalled();
- expect(findComponent(BlobContentEdit).props('value')).toBe('');
- });
+ it('shows blob header', () => {
+ const { canDelete = true, showDelete = false } = props;
+
+ expect(findHeader().props()).toMatchObject({
+ canDelete,
+ showDelete,
});
+ });
- it('does not fetch content for new snippet', () => {
- bootstrapForNewSnippet();
+ it(`handles loading icon (show=${showLoading})`, () => {
+ expect(findLoadingIcon().exists()).toBe(showLoading);
+ });
- return waitForPromises().then(() => {
- // we keep using waitForPromises to make sure we do not run failed test
- expect(findComponent(BlobHeaderEdit).props('value')).toBe('');
- expect(findComponent(BlobContentEdit).props('value')).toBe('');
- expect(joinPaths).not.toHaveBeenCalled();
+ it(`handles content (show=${showContent})`, () => {
+ expect(findContent().exists()).toBe(showContent);
+
+ if (showContent) {
+ expect(findContent().props()).toEqual({
+ value: TEST_BLOB_LOADED.content,
+ fileGlobalId: TEST_BLOB_LOADED.id,
+ fileName: TEST_BLOB_LOADED.path,
});
- });
+ }
});
});
});
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index c8f1c8fc8a9..9c4b2734a3f 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -1,7 +1,14 @@
+import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
+import {
+ Blob as BlobMock,
+ SimpleViewerMock,
+ RichViewerMock,
+ 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 BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import BlobContent from '~/blob/components/blob_content.vue';
import {
BLOB_RENDER_EVENT_LOAD,
@@ -9,13 +16,7 @@ import {
BLOB_RENDER_ERRORS,
} from '~/blob/components/constants';
import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
-import {
- SNIPPET_VISIBILITY_PRIVATE,
- SNIPPET_VISIBILITY_INTERNAL,
- SNIPPET_VISIBILITY_PUBLIC,
-} from '~/snippets/constants';
-
-import { Blob as BlobMock, SimpleViewerMock, RichViewerMock } from 'jest/blob/components/mock_data';
+import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
describe('Blob Embeddable', () => {
let wrapper;
@@ -72,18 +73,6 @@ describe('Blob Embeddable', () => {
expect(wrapper.find(BlobContent).exists()).toBe(true);
});
- it.each([SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PRIVATE, 'foo'])(
- 'does not render blob-embeddable by default',
- visibilityLevel => {
- createComponent({
- snippetProps: {
- visibilityLevel,
- },
- });
- expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
- },
- );
-
it('sets simple viewer correctly', () => {
createComponent();
expect(wrapper.find(SimpleViewer).exists()).toBe(true);
@@ -128,6 +117,59 @@ describe('Blob Embeddable', () => {
expect(wrapper.find(BlobHeader).props('hasRenderError')).toBe(true);
});
+ describe('bob content in multi-file scenario', () => {
+ const SimpleBlobContentMock2 = {
+ ...SimpleBlobContentMock,
+ plainData: 'Another Plain Foo',
+ };
+ const RichBlobContentMock2 = {
+ ...SimpleBlobContentMock,
+ richData: 'Another Rich Foo',
+ };
+
+ it.each`
+ snippetBlobs | description | currentBlob | expectedContent
+ ${[SimpleBlobContentMock]} | ${'one existing textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
+ ${[RichBlobContentMock]} | ${'one existing rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
+ ${[SimpleBlobContentMock, RichBlobContentMock]} | ${'mixed blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
+ ${[SimpleBlobContentMock, RichBlobContentMock]} | ${'mixed blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
+ ${[SimpleBlobContentMock, SimpleBlobContentMock2]} | ${'textual blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
+ ${[RichBlobContentMock, RichBlobContentMock2]} | ${'rich blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
+ `(
+ 'renders correct content for $description',
+ async ({ snippetBlobs, currentBlob, expectedContent }) => {
+ const apolloData = {
+ snippets: {
+ edges: [
+ {
+ node: {
+ blobs: snippetBlobs,
+ },
+ },
+ ],
+ },
+ };
+ createComponent({
+ blob: {
+ ...BlobMock,
+ path: currentBlob.path,
+ },
+ });
+
+ // mimic apollo's update
+ wrapper.setData({
+ blobContent: wrapper.vm.onContentUpdate(apolloData),
+ });
+
+ await nextTick();
+
+ const findContent = () => wrapper.find(BlobContent);
+
+ expect(findContent().props('content')).toBe(expectedContent);
+ },
+ );
+ });
+
describe('URLS with hash', () => {
beforeEach(() => {
window.location.hash = '#LC2';
diff --git a/spec/frontend/snippets/components/snippet_description_edit_spec.js b/spec/frontend/snippets/components/snippet_description_edit_spec.js
index 816ab4e48de..ff75515e71a 100644
--- a/spec/frontend/snippets/components/snippet_description_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_description_edit_spec.js
@@ -1,6 +1,6 @@
+import { shallowMount } from '@vue/test-utils';
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { shallowMount } from '@vue/test-utils';
describe('Snippet Description Edit component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_description_view_spec.js b/spec/frontend/snippets/components/snippet_description_view_spec.js
index 46467ef311e..14f116f2aaf 100644
--- a/spec/frontend/snippets/components/snippet_description_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_description_view_spec.js
@@ -1,5 +1,5 @@
-import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
import { shallowMount } from '@vue/test-utils';
+import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
describe('Snippet Description component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 0825da92118..da8cb2e6a8d 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -1,46 +1,19 @@
-import SnippetHeader from '~/snippets/components/snippet_header.vue';
-import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
import { ApolloMutation } from 'vue-apollo';
import { GlButton, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
+import waitForPromises from 'helpers/wait_for_promises';
+import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
+import SnippetHeader from '~/snippets/components/snippet_header.vue';
describe('Snippet header component', () => {
let wrapper;
- const snippet = {
- id: 'gid://gitlab/PersonalSnippet/50',
- title: 'The property of Thor',
- visibilityLevel: 'private',
- webUrl: 'http://personal.dev.null/42',
- userPermissions: {
- adminSnippet: true,
- updateSnippet: true,
- reportSnippet: false,
- },
- project: null,
- author: {
- name: 'Thor Odinson',
- },
- blobs: [Blob],
- };
- const mutationVariables = {
- mutation: DeleteSnippetMutation,
- variables: {
- id: snippet.id,
- },
- };
- const errorMsg = 'Foo bar';
- const err = { message: errorMsg };
-
- const resolveMutate = jest.fn(() =>
- Promise.resolve({ data: { destroySnippet: { errors: [] } } }),
- );
- const rejectMutation = jest.fn(() => Promise.reject(err));
-
- const mutationTypes = {
- RESOLVE: resolveMutate,
- REJECT: rejectMutation,
- };
+ let snippet;
+ let mutationTypes;
+ let mutationVariables;
+
+ let errorMsg;
+ let err;
function createComponent({
loading = false,
@@ -63,7 +36,7 @@ describe('Snippet header component', () => {
mutate: mutationRes,
};
- wrapper = shallowMount(SnippetHeader, {
+ wrapper = mount(SnippetHeader, {
mocks: { $apollo },
propsData: {
snippet: {
@@ -76,6 +49,41 @@ describe('Snippet header component', () => {
});
}
+ beforeEach(() => {
+ snippet = {
+ id: 'gid://gitlab/PersonalSnippet/50',
+ title: 'The property of Thor',
+ visibilityLevel: 'private',
+ webUrl: 'http://personal.dev.null/42',
+ userPermissions: {
+ adminSnippet: true,
+ updateSnippet: true,
+ reportSnippet: false,
+ },
+ project: null,
+ author: {
+ name: 'Thor Odinson',
+ },
+ blobs: [Blob],
+ createdAt: new Date(Date.now() - 32 * 24 * 3600 * 1000).toISOString(),
+ };
+
+ mutationVariables = {
+ mutation: DeleteSnippetMutation,
+ variables: {
+ id: snippet.id,
+ },
+ };
+
+ errorMsg = 'Foo bar';
+ err = { message: errorMsg };
+
+ mutationTypes = {
+ RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })),
+ REJECT: jest.fn(() => Promise.reject(err)),
+ };
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -85,6 +93,23 @@ describe('Snippet header component', () => {
expect(wrapper.find('.detail-page-header').exists()).toBe(true);
});
+ it('renders a message showing snippet creation date and author', () => {
+ createComponent();
+
+ const text = wrapper.find('[data-testid="authored-message"]').text();
+ expect(text).toContain('Authored 1 month ago by');
+ expect(text).toContain('Thor Odinson');
+ });
+
+ it('renders a message showing only snippet creation date if author is null', () => {
+ snippet.author = null;
+
+ createComponent();
+
+ const text = wrapper.find('[data-testid="authored-message"]').text();
+ expect(text).toBe('Authored 1 month ago');
+ });
+
it('renders action buttons based on permissions', () => {
createComponent({
permissions: {
@@ -163,14 +188,15 @@ describe('Snippet header component', () => {
expect(mutationTypes.RESOLVE).toHaveBeenCalledWith(mutationVariables);
});
- it('sets error message if mutation fails', () => {
+ it('sets error message if mutation fails', async () => {
createComponent({ mutationRes: mutationTypes.REJECT });
expect(Boolean(wrapper.vm.errorMessage)).toBe(false);
wrapper.vm.deleteSnippet();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.errorMessage).toEqual(errorMsg);
- });
+
+ await waitForPromises();
+
+ expect(wrapper.vm.errorMessage).toEqual(errorMsg);
});
describe('in case of successful mutation, closes modal and redirects to correct listing', () => {
@@ -199,7 +225,7 @@ describe('Snippet header component', () => {
},
}).then(() => {
expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
- expect(window.location.pathname).toBe(`${fullPath}/snippets`);
+ expect(window.location.pathname).toBe(`${fullPath}/-/snippets`);
});
});
});
diff --git a/spec/frontend/snippets/components/snippet_title_spec.js b/spec/frontend/snippets/components/snippet_title_spec.js
index 88261a75f6c..f201cfb19b7 100644
--- a/spec/frontend/snippets/components/snippet_title_spec.js
+++ b/spec/frontend/snippets/components/snippet_title_spec.js
@@ -1,7 +1,7 @@
-import SnippetTitle from '~/snippets/components/snippet_title.vue';
-import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
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';
describe('Snippet header component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
index 0bdef71bc08..a8df13787a5 100644
--- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
@@ -1,12 +1,12 @@
-import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import {
SNIPPET_VISIBILITY,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
-import { mount, shallowMount } from '@vue/test-utils';
describe('Snippet Visibility Edit component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/test_utils.js b/spec/frontend/snippets/test_utils.js
new file mode 100644
index 00000000000..86262723157
--- /dev/null
+++ b/spec/frontend/snippets/test_utils.js
@@ -0,0 +1,76 @@
+import {
+ SNIPPET_BLOB_ACTION_CREATE,
+ SNIPPET_BLOB_ACTION_UPDATE,
+ SNIPPET_BLOB_ACTION_MOVE,
+ SNIPPET_BLOB_ACTION_DELETE,
+} from '~/snippets/constants';
+
+const CONTENT_1 = 'Lorem ipsum dolar\nSit amit\n\nGoodbye!\n';
+const CONTENT_2 = 'Lorem ipsum dolar sit amit.\n\nGoodbye!\n';
+
+export const testEntries = {
+ created: {
+ id: 'blob_1',
+ diff: {
+ action: SNIPPET_BLOB_ACTION_CREATE,
+ filePath: '/new/file',
+ previousPath: '/new/file',
+ content: CONTENT_1,
+ },
+ },
+ deleted: {
+ id: 'blob_2',
+ diff: {
+ action: SNIPPET_BLOB_ACTION_DELETE,
+ filePath: '/src/delete/me',
+ previousPath: '/src/delete/me',
+ content: CONTENT_1,
+ },
+ },
+ updated: {
+ id: 'blob_3',
+ origContent: CONTENT_1,
+ diff: {
+ action: SNIPPET_BLOB_ACTION_UPDATE,
+ filePath: '/lorem.md',
+ previousPath: '/lorem.md',
+ content: CONTENT_2,
+ },
+ },
+ renamed: {
+ id: 'blob_4',
+ diff: {
+ action: SNIPPET_BLOB_ACTION_MOVE,
+ filePath: '/dolar.md',
+ previousPath: '/ipsum.md',
+ content: CONTENT_1,
+ },
+ },
+ renamedAndUpdated: {
+ id: 'blob_5',
+ origContent: CONTENT_1,
+ diff: {
+ action: SNIPPET_BLOB_ACTION_MOVE,
+ filePath: '/sit.md',
+ previousPath: '/sit/amit.md',
+ content: CONTENT_2,
+ },
+ },
+};
+
+export const createBlobFromTestEntry = ({ diff, origContent }, isOrig = false) => ({
+ content: isOrig && origContent ? origContent : diff.content,
+ path: isOrig ? diff.previousPath : diff.filePath,
+});
+
+export const createBlobsFromTestEntries = (entries, isOrig = false) =>
+ entries.reduce(
+ (acc, entry) =>
+ Object.assign(acc, {
+ [entry.id]: {
+ id: entry.id,
+ ...createBlobFromTestEntry(entry, isOrig),
+ },
+ }),
+ {},
+ );
diff --git a/spec/frontend/snippets/utils/blob_spec.js b/spec/frontend/snippets/utils/blob_spec.js
new file mode 100644
index 00000000000..c20cf2e6102
--- /dev/null
+++ b/spec/frontend/snippets/utils/blob_spec.js
@@ -0,0 +1,63 @@
+import { cloneDeep } from 'lodash';
+import { decorateBlob, createBlob, diffAll } from '~/snippets/utils/blob';
+import { testEntries, createBlobsFromTestEntries } from '../test_utils';
+
+jest.mock('lodash/uniqueId', () => arg => `${arg}fakeUniqueId`);
+
+const TEST_RAW_BLOB = {
+ rawPath: '/test/blob/7/raw',
+};
+
+describe('~/snippets/utils/blob', () => {
+ describe('decorateBlob', () => {
+ it('should decorate the given object with local blob properties', () => {
+ const orig = cloneDeep(TEST_RAW_BLOB);
+
+ expect(decorateBlob(orig)).toEqual({
+ ...TEST_RAW_BLOB,
+ id: 'blob_local_fakeUniqueId',
+ isLoaded: false,
+ content: '',
+ });
+ });
+ });
+
+ describe('createBlob', () => {
+ it('should create an empty local blob', () => {
+ expect(createBlob()).toEqual({
+ id: 'blob_local_fakeUniqueId',
+ isLoaded: true,
+ content: '',
+ path: '',
+ });
+ });
+ });
+
+ describe('diffAll', () => {
+ it('should create diff from original files', () => {
+ const origBlobs = createBlobsFromTestEntries(
+ [
+ testEntries.deleted,
+ testEntries.updated,
+ testEntries.renamed,
+ testEntries.renamedAndUpdated,
+ ],
+ true,
+ );
+ const blobs = createBlobsFromTestEntries([
+ testEntries.created,
+ testEntries.updated,
+ testEntries.renamed,
+ testEntries.renamedAndUpdated,
+ ]);
+
+ expect(diffAll(blobs, origBlobs)).toEqual([
+ testEntries.deleted.diff,
+ testEntries.created.diff,
+ testEntries.updated.diff,
+ testEntries.renamed.diff,
+ testEntries.renamedAndUpdated.diff,
+ ]);
+ });
+ });
+});