summaryrefslogtreecommitdiff
path: root/spec/frontend/ide
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 07:08:36 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 07:08:36 +0000
commit48aff82709769b098321c738f3444b9bdaa694c6 (patch)
treee00c7c43e2d9b603a5a6af576b1685e400410dee /spec/frontend/ide
parent879f5329ee916a948223f8f43d77fba4da6cd028 (diff)
downloadgitlab-ce-48aff82709769b098321c738f3444b9bdaa694c6.tar.gz
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'spec/frontend/ide')
-rw-r--r--spec/frontend/ide/components/commit_sidebar/actions_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js37
-rw-r--r--spec/frontend/ide/components/ide_review_spec.js78
-rw-r--r--spec/frontend/ide/components/ide_side_bar_spec.js81
-rw-r--r--spec/frontend/ide/components/ide_tree_list_spec.js8
-rw-r--r--spec/frontend/ide/components/ide_tree_spec.js41
-rw-r--r--spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap2
-rw-r--r--spec/frontend/ide/components/jobs/detail/scroll_button_spec.js2
-rw-r--r--spec/frontend/ide/components/new_dropdown/upload_spec.js17
-rw-r--r--spec/frontend/ide/components/repo_commit_section_spec.js21
-rw-r--r--spec/frontend/ide/lib/errors_spec.js46
-rw-r--r--spec/frontend/ide/lib/languages/hcl_spec.js290
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js14
-rw-r--r--spec/frontend/ide/stores/getters_spec.js10
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js82
-rw-r--r--spec/frontend/ide/stores/mutations/file_spec.js26
-rw-r--r--spec/frontend/ide/stores/utils_spec.js10
-rw-r--r--spec/frontend/ide/utils_spec.js40
18 files changed, 632 insertions, 177 deletions
diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
index a303e2b9bee..0003e13c92f 100644
--- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
@@ -83,12 +83,12 @@ describe('IDE commit sidebar actions', () => {
});
});
- describe('commitToCurrentBranchText', () => {
+ describe('currentBranchText', () => {
it('escapes current branch', () => {
const injectedSrc = '<img src="x" />';
createComponent({ currentBranchId: injectedSrc });
- expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc);
+ expect(vm.currentBranchText).not.toContain(injectedSrc);
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index 56667d6b03d..abd7e3bb8fc 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -7,7 +7,12 @@ import { createStore } from '~/ide/stores';
import consts from '~/ide/stores/modules/commit/constants';
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
import { leftSidebarViews } from '~/ide/constants';
-import { createCodeownersCommitError, createUnexpectedCommitError } from '~/ide/lib/errors';
+import {
+ createCodeownersCommitError,
+ createUnexpectedCommitError,
+ createBranchChangedCommitError,
+ branchAlreadyExistsCommitError,
+} from '~/ide/lib/errors';
describe('IDE commit form', () => {
const Component = Vue.extend(CommitForm);
@@ -290,20 +295,30 @@ describe('IDE commit form', () => {
jest.spyOn(vm.$store, 'dispatch').mockReturnValue(Promise.resolve());
});
- it('updates commit action and commits', async () => {
- store.state.commit.commitError = createCodeownersCommitError('test message');
+ const commitActions = [
+ ['commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH],
+ ['commit/commitChanges'],
+ ];
- await vm.$nextTick();
+ it.each`
+ commitError | expectedActions
+ ${createCodeownersCommitError} | ${commitActions}
+ ${createBranchChangedCommitError} | ${commitActions}
+ ${branchAlreadyExistsCommitError} | ${[['commit/addSuffixToBranchName'], ...commitActions]}
+ `(
+ 'updates commit action and commits for error: $commitError',
+ async ({ commitError, expectedActions }) => {
+ store.state.commit.commitError = commitError('test message');
- getByText(document.body, 'Create new branch').click();
+ await vm.$nextTick();
- await waitForPromises();
+ getByText(document.body, 'Create new branch').click();
- expect(vm.$store.dispatch.mock.calls).toEqual([
- ['commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH],
- ['commit/commitChanges', undefined],
- ]);
- });
+ await waitForPromises();
+
+ expect(vm.$store.dispatch.mock.calls).toEqual(expectedActions);
+ },
+ );
});
});
diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js
index c9ac2ac423d..bcc98669427 100644
--- a/spec/frontend/ide/components/ide_review_spec.js
+++ b/spec/frontend/ide/components/ide_review_spec.js
@@ -1,14 +1,19 @@
import Vue from 'vue';
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
import IdeReview from '~/ide/components/ide_review.vue';
+import EditorModeDropdown from '~/ide/components/editor_mode_dropdown.vue';
import { createStore } from '~/ide/stores';
-import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { trimText } from '../../helpers/text_helper';
+import { keepAlive } from '../../helpers/keep_alive_component_helper';
import { file } from '../helpers';
import { projectData } from '../mock_data';
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
describe('IDE review mode', () => {
- const Component = Vue.extend(IdeReview);
- let vm;
+ let wrapper;
let store;
beforeEach(() => {
@@ -21,15 +26,53 @@ describe('IDE review mode', () => {
loading: false,
});
- vm = createComponentWithStore(Component, store).$mount();
+ wrapper = mount(keepAlive(IdeReview), {
+ store,
+ localVue,
+ });
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders list of files', () => {
- expect(vm.$el.textContent).toContain('fileName');
+ expect(wrapper.text()).toContain('fileName');
+ });
+
+ describe('activated', () => {
+ let inititializeSpy;
+
+ beforeEach(async () => {
+ inititializeSpy = jest.spyOn(wrapper.find(IdeReview).vm, 'initialize');
+ store.state.viewer = 'editor';
+
+ await wrapper.vm.reactivate();
+ });
+
+ it('re initializes the component', () => {
+ expect(inititializeSpy).toHaveBeenCalled();
+ });
+
+ it('updates viewer to "diff" by default', () => {
+ expect(store.state.viewer).toBe('diff');
+ });
+
+ describe('merge request is defined', () => {
+ beforeEach(async () => {
+ store.state.currentMergeRequestId = '1';
+ store.state.projects.abcproject.mergeRequests['1'] = {
+ iid: 123,
+ web_url: 'testing123',
+ };
+
+ await wrapper.vm.reactivate();
+ });
+
+ it('updates viewer to "mrdiff"', async () => {
+ expect(store.state.viewer).toBe('mrdiff');
+ });
+ });
});
describe('merge request', () => {
@@ -40,32 +83,27 @@ describe('IDE review mode', () => {
web_url: 'testing123',
};
- return vm.$nextTick();
+ return wrapper.vm.$nextTick();
});
it('renders edit dropdown', () => {
- expect(vm.$el.querySelector('.btn')).not.toBe(null);
+ expect(wrapper.find(EditorModeDropdown).exists()).toBe(true);
});
- it('renders merge request link & IID', () => {
+ it('renders merge request link & IID', async () => {
store.state.viewer = 'mrdiff';
- return vm.$nextTick(() => {
- const link = vm.$el.querySelector('.ide-review-sub-header');
+ await wrapper.vm.$nextTick();
- expect(link.querySelector('a').getAttribute('href')).toBe('testing123');
- expect(trimText(link.textContent)).toBe('Merge request (!123)');
- });
+ expect(trimText(wrapper.text())).toContain('Merge request (!123)');
});
- it('changes text to latest changes when viewer is not mrdiff', () => {
+ it('changes text to latest changes when viewer is not mrdiff', async () => {
store.state.viewer = 'diff';
- return vm.$nextTick(() => {
- expect(trimText(vm.$el.querySelector('.ide-review-sub-header').textContent)).toBe(
- 'Latest changes',
- );
- });
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.text()).toContain('Latest changes');
});
});
});
diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js
index 67257b40879..86e4e8d8f89 100644
--- a/spec/frontend/ide/components/ide_side_bar_spec.js
+++ b/spec/frontend/ide/components/ide_side_bar_spec.js
@@ -1,57 +1,88 @@
-import Vue from 'vue';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { GlSkeletonLoading } from '@gitlab/ui';
import { createStore } from '~/ide/stores';
-import ideSidebar from '~/ide/components/ide_side_bar.vue';
+import IdeSidebar from '~/ide/components/ide_side_bar.vue';
+import IdeTree from '~/ide/components/ide_tree.vue';
+import RepoCommitSection from '~/ide/components/repo_commit_section.vue';
import { leftSidebarViews } from '~/ide/constants';
import { projectData } from '../mock_data';
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
describe('IdeSidebar', () => {
- let vm;
+ let wrapper;
let store;
- beforeEach(() => {
+ function createComponent() {
store = createStore();
- const Component = Vue.extend(ideSidebar);
-
store.state.currentProjectId = 'abcproject';
store.state.projects.abcproject = projectData;
- vm = createComponentWithStore(Component, store).$mount();
- });
+ return mount(IdeSidebar, {
+ store,
+ localVue,
+ });
+ }
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
it('renders a sidebar', () => {
- expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
+ wrapper = createComponent();
+
+ expect(wrapper.find('[data-testid="ide-side-bar-inner"]').exists()).toBe(true);
});
- it('renders loading icon component', done => {
- vm.$store.state.loading = true;
+ it('renders loading components', async () => {
+ wrapper = createComponent();
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
- expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
+ store.state.loading = true;
- done();
- });
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.findAll(GlSkeletonLoading)).toHaveLength(3);
});
describe('activityBarComponent', () => {
it('renders tree component', () => {
- expect(vm.$el.querySelector('.ide-file-list')).not.toBeNull();
+ wrapper = createComponent();
+
+ expect(wrapper.find(IdeTree).exists()).toBe(true);
});
- it('renders commit component', done => {
- vm.$store.state.currentActivityView = leftSidebarViews.commit.name;
+ it('renders commit component', async () => {
+ wrapper = createComponent();
+
+ store.state.currentActivityView = leftSidebarViews.commit.name;
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.multi-file-commit-panel-section')).not.toBeNull();
+ await wrapper.vm.$nextTick();
- done();
- });
+ expect(wrapper.find(RepoCommitSection).exists()).toBe(true);
});
});
+
+ it('keeps the current activity view components alive', async () => {
+ wrapper = createComponent();
+
+ const ideTreeComponent = wrapper.find(IdeTree).element;
+
+ store.state.currentActivityView = leftSidebarViews.commit.name;
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(IdeTree).exists()).toBe(false);
+ expect(wrapper.find(RepoCommitSection).exists()).toBe(true);
+
+ store.state.currentActivityView = leftSidebarViews.edit.name;
+
+ await wrapper.vm.$nextTick();
+
+ // reference to the elements remains the same, meaning the components were kept alive
+ expect(wrapper.find(IdeTree).element).toEqual(ideTreeComponent);
+ });
});
diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js
index 4593ef6049b..dd57a5c5f4d 100644
--- a/spec/frontend/ide/components/ide_tree_list_spec.js
+++ b/spec/frontend/ide/components/ide_tree_list_spec.js
@@ -38,15 +38,9 @@ describe('IDE tree list', () => {
beforeEach(() => {
bootstrapWithTree();
- jest.spyOn(vm, 'updateViewer');
-
vm.$mount();
});
- it('updates viewer on mount', () => {
- expect(vm.updateViewer).toHaveBeenCalledWith('edit');
- });
-
it('renders loading indicator', done => {
store.state.trees['abcproject/master'].loading = true;
@@ -67,8 +61,6 @@ describe('IDE tree list', () => {
beforeEach(() => {
bootstrapWithTree(emptyBranchTree);
- jest.spyOn(vm, 'updateViewer');
-
vm.$mount();
});
diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js
index 899daa0bf57..ad00dec2e48 100644
--- a/spec/frontend/ide/components/ide_tree_spec.js
+++ b/spec/frontend/ide/components/ide_tree_spec.js
@@ -1,19 +1,22 @@
import Vue from 'vue';
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
import IdeTree from '~/ide/components/ide_tree.vue';
import { createStore } from '~/ide/stores';
-import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { keepAlive } from '../../helpers/keep_alive_component_helper';
import { file } from '../helpers';
import { projectData } from '../mock_data';
-describe('IdeRepoTree', () => {
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('IdeTree', () => {
let store;
- let vm;
+ let wrapper;
beforeEach(() => {
store = createStore();
- const IdeRepoTree = Vue.extend(IdeTree);
-
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = { ...projectData };
@@ -22,14 +25,36 @@ describe('IdeRepoTree', () => {
loading: false,
});
- vm = createComponentWithStore(IdeRepoTree, store).$mount();
+ wrapper = mount(keepAlive(IdeTree), {
+ store,
+ localVue,
+ });
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders list of files', () => {
- expect(vm.$el.textContent).toContain('fileName');
+ expect(wrapper.text()).toContain('fileName');
+ });
+
+ describe('activated', () => {
+ let inititializeSpy;
+
+ beforeEach(async () => {
+ inititializeSpy = jest.spyOn(wrapper.find(IdeTree).vm, 'initialize');
+ store.state.viewer = 'diff';
+
+ await wrapper.vm.reactivate();
+ });
+
+ it('re initializes the component', () => {
+ expect(inititializeSpy).toHaveBeenCalled();
+ });
+
+ it('updates viewer to "editor" by default', () => {
+ expect(store.state.viewer).toBe('editor');
+ });
});
});
diff --git a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
index a65d9e6f78b..faa70982fac 100644
--- a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
+++ b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
@@ -16,8 +16,6 @@ exports[`IDE pipeline stage renders stage details & icon 1`] = `
<strong
class="gl-ml-3 text-truncate"
data-container="body"
- data-original-title=""
- title=""
>
build
diff --git a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
index 42526590ebb..57174181a3d 100644
--- a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
@@ -31,7 +31,7 @@ describe('IDE job log scroll button', () => {
});
it('returns proper title', () => {
- expect(wrapper.attributes('data-original-title')).toBe(title);
+ expect(wrapper.attributes('title')).toBe(title);
});
});
diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js
index ae497106f73..3f3784dbb3a 100644
--- a/spec/frontend/ide/components/new_dropdown/upload_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js
@@ -59,14 +59,11 @@ describe('new dropdown upload', () => {
result: 'base64,cGxhaW4gdGV4dA==',
};
const binaryTarget = {
- result: 'base64,w4I=',
+ result: 'base64,8PDw8A==', // ðððð
};
- const textFile = new File(['plain text'], 'textFile');
- const binaryFile = {
- name: 'binaryFile',
- type: 'image/png',
- };
+ const textFile = new File(['plain text'], 'textFile');
+ const binaryFile = new File(['😺'], 'binaryFile');
beforeEach(() => {
jest.spyOn(FileReader.prototype, 'readAsText');
@@ -92,16 +89,16 @@ describe('new dropdown upload', () => {
.catch(done.fail);
});
- it('splits content on base64 if binary', () => {
+ it('creates a blob URL for the content if binary', () => {
vm.createFile(binaryTarget, binaryFile);
- expect(FileReader.prototype.readAsText).not.toHaveBeenCalledWith(textFile);
+ expect(FileReader.prototype.readAsText).not.toHaveBeenCalled();
expect(vm.$emit).toHaveBeenCalledWith('create', {
name: binaryFile.name,
type: 'blob',
- content: binaryTarget.result.split('base64,')[1],
- rawPath: binaryTarget.result,
+ content: 'ðððð',
+ rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b',
});
});
});
diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js
index 3b837622720..096079308cd 100644
--- a/spec/frontend/ide/components/repo_commit_section_spec.js
+++ b/spec/frontend/ide/components/repo_commit_section_spec.js
@@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils';
import { createStore } from '~/ide/stores';
import { createRouter } from '~/ide/ide_router';
+import { keepAlive } from '../../helpers/keep_alive_component_helper';
import RepoCommitSection from '~/ide/components/repo_commit_section.vue';
import EmptyState from '~/ide/components/commit_sidebar/empty_state.vue';
import { stageKeys } from '~/ide/constants';
@@ -14,7 +15,7 @@ describe('RepoCommitSection', () => {
let store;
function createComponent() {
- wrapper = mount(RepoCommitSection, { store });
+ wrapper = mount(keepAlive(RepoCommitSection), { store });
}
function setupDefaultState() {
@@ -64,6 +65,7 @@ describe('RepoCommitSection', () => {
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
describe('empty state', () => {
@@ -168,4 +170,21 @@ describe('RepoCommitSection', () => {
expect(wrapper.find(EmptyState).exists()).toBe(false);
});
});
+
+ describe('activated', () => {
+ let inititializeSpy;
+
+ beforeEach(async () => {
+ createComponent();
+
+ inititializeSpy = jest.spyOn(wrapper.find(RepoCommitSection).vm, 'initialize');
+ store.state.viewer = 'diff';
+
+ await wrapper.vm.reactivate();
+ });
+
+ it('re initializes the component', () => {
+ expect(inititializeSpy).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/ide/lib/errors_spec.js b/spec/frontend/ide/lib/errors_spec.js
index 8c3fb378302..733d5a5da3c 100644
--- a/spec/frontend/ide/lib/errors_spec.js
+++ b/spec/frontend/ide/lib/errors_spec.js
@@ -2,6 +2,7 @@ import {
createUnexpectedCommitError,
createCodeownersCommitError,
createBranchChangedCommitError,
+ branchAlreadyExistsCommitError,
parseCommitError,
} from '~/ide/lib/errors';
@@ -21,35 +22,22 @@ describe('~/ide/lib/errors', () => {
},
});
- describe('createCodeownersCommitError', () => {
- it('uses given message', () => {
- expect(createCodeownersCommitError(TEST_MESSAGE)).toEqual({
- title: 'CODEOWNERS rule violation',
- messageHTML: TEST_MESSAGE,
- canCreateBranch: true,
- });
- });
+ const NEW_BRANCH_SUFFIX = `<br/><br/>Would you like to create a new branch?`;
+ const AUTOGENERATE_SUFFIX = `<br/><br/>Would you like to try auto-generating a branch name?`;
- it('escapes special chars', () => {
- expect(createCodeownersCommitError(TEST_SPECIAL)).toEqual({
- title: 'CODEOWNERS rule violation',
- messageHTML: TEST_SPECIAL_ESCAPED,
- canCreateBranch: true,
- });
- });
- });
-
- describe('createBranchChangedCommitError', () => {
- it.each`
- message | expectedMessage
- ${TEST_MESSAGE} | ${`${TEST_MESSAGE}<br/><br/>Would you like to create a new branch?`}
- ${TEST_SPECIAL} | ${`${TEST_SPECIAL_ESCAPED}<br/><br/>Would you like to create a new branch?`}
- `('uses given message="$message"', ({ message, expectedMessage }) => {
- expect(createBranchChangedCommitError(message)).toEqual({
- title: 'Branch changed',
- messageHTML: expectedMessage,
- canCreateBranch: true,
- });
+ it.each`
+ fn | title | message | messageHTML
+ ${createCodeownersCommitError} | ${'CODEOWNERS rule violation'} | ${TEST_MESSAGE} | ${TEST_MESSAGE}
+ ${createCodeownersCommitError} | ${'CODEOWNERS rule violation'} | ${TEST_SPECIAL} | ${TEST_SPECIAL_ESCAPED}
+ ${branchAlreadyExistsCommitError} | ${'Branch already exists'} | ${TEST_MESSAGE} | ${`${TEST_MESSAGE}${AUTOGENERATE_SUFFIX}`}
+ ${branchAlreadyExistsCommitError} | ${'Branch already exists'} | ${TEST_SPECIAL} | ${`${TEST_SPECIAL_ESCAPED}${AUTOGENERATE_SUFFIX}`}
+ ${createBranchChangedCommitError} | ${'Branch changed'} | ${TEST_MESSAGE} | ${`${TEST_MESSAGE}${NEW_BRANCH_SUFFIX}`}
+ ${createBranchChangedCommitError} | ${'Branch changed'} | ${TEST_SPECIAL} | ${`${TEST_SPECIAL_ESCAPED}${NEW_BRANCH_SUFFIX}`}
+ `('$fn escapes and uses given message="$message"', ({ fn, title, message, messageHTML }) => {
+ expect(fn(message)).toEqual({
+ title,
+ messageHTML,
+ primaryAction: { text: 'Create new branch', callback: expect.any(Function) },
});
});
@@ -60,7 +48,7 @@ describe('~/ide/lib/errors', () => {
${{}} | ${createUnexpectedCommitError()}
${{ response: {} }} | ${createUnexpectedCommitError()}
${{ response: { data: {} } }} | ${createUnexpectedCommitError()}
- ${createResponseError('test')} | ${createUnexpectedCommitError()}
+ ${createResponseError(TEST_MESSAGE)} | ${createUnexpectedCommitError(TEST_MESSAGE)}
${createResponseError(CODEOWNERS_MESSAGE)} | ${createCodeownersCommitError(CODEOWNERS_MESSAGE)}
${createResponseError(CHANGED_MESSAGE)} | ${createBranchChangedCommitError(CHANGED_MESSAGE)}
`('parses message into error object with "$message"', ({ message, expectation }) => {
diff --git a/spec/frontend/ide/lib/languages/hcl_spec.js b/spec/frontend/ide/lib/languages/hcl_spec.js
new file mode 100644
index 00000000000..a39673a3225
--- /dev/null
+++ b/spec/frontend/ide/lib/languages/hcl_spec.js
@@ -0,0 +1,290 @@
+import { editor } from 'monaco-editor';
+import { registerLanguages } from '~/ide/utils';
+import hcl from '~/ide/lib/languages/hcl';
+
+describe('tokenization for .tf files', () => {
+ beforeEach(() => {
+ registerLanguages(hcl);
+ });
+
+ it.each([
+ ['// Foo', [[{ language: 'hcl', offset: 0, type: 'comment.hcl' }]]],
+ ['/* Bar */', [[{ language: 'hcl', offset: 0, type: 'comment.hcl' }]]],
+ ['/*', [[{ language: 'hcl', offset: 0, type: 'comment.hcl' }]]],
+ [
+ 'foo = "bar"',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'operator.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'string.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'variable "foo" {',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'type.hcl' },
+ { language: 'hcl', offset: 8, type: '' },
+ { language: 'hcl', offset: 9, type: 'string.hcl' },
+ { language: 'hcl', offset: 14, type: '' },
+ { language: 'hcl', offset: 15, type: 'delimiter.curly.hcl' },
+ ],
+ ],
+ ],
+ [
+ // eslint-disable-next-line no-template-curly-in-string
+ ' api_key = "${var.foo}"',
+ [
+ [
+ { language: 'hcl', offset: 0, type: '' },
+ { language: 'hcl', offset: 2, type: 'variable.hcl' },
+ { language: 'hcl', offset: 9, type: '' },
+ { language: 'hcl', offset: 10, type: 'operator.hcl' },
+ { language: 'hcl', offset: 11, type: '' },
+ { language: 'hcl', offset: 12, type: 'string.hcl' },
+ { language: 'hcl', offset: 13, type: 'delimiter.hcl' },
+ { language: 'hcl', offset: 15, type: 'keyword.var.hcl' },
+ { language: 'hcl', offset: 18, type: 'delimiter.hcl' },
+ { language: 'hcl', offset: 19, type: 'variable.hcl' },
+ { language: 'hcl', offset: 22, type: 'delimiter.hcl' },
+ { language: 'hcl', offset: 23, type: 'string.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'resource "aws_security_group" "firewall" {',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'type.hcl' },
+ { language: 'hcl', offset: 8, type: '' },
+ { language: 'hcl', offset: 9, type: 'string.hcl' },
+ { language: 'hcl', offset: 29, type: '' },
+ { language: 'hcl', offset: 30, type: 'string.hcl' },
+ { language: 'hcl', offset: 40, type: '' },
+ { language: 'hcl', offset: 41, type: 'delimiter.curly.hcl' },
+ ],
+ ],
+ ],
+ [
+ ' network_interface {',
+ [
+ [
+ { language: 'hcl', offset: 0, type: '' },
+ { language: 'hcl', offset: 2, type: 'identifier.hcl' },
+ { language: 'hcl', offset: 20, type: 'delimiter.curly.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'foo = [1, 2, "foo"]',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'operator.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'delimiter.square.hcl' },
+ { language: 'hcl', offset: 7, type: 'number.hcl' },
+ { language: 'hcl', offset: 8, type: 'delimiter.hcl' },
+ { language: 'hcl', offset: 9, type: '' },
+ { language: 'hcl', offset: 10, type: 'number.hcl' },
+ { language: 'hcl', offset: 11, type: 'delimiter.hcl' },
+ { language: 'hcl', offset: 12, type: '' },
+ { language: 'hcl', offset: 13, type: 'string.hcl' },
+ { language: 'hcl', offset: 18, type: 'delimiter.square.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'resource "foo" "bar" {}',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'type.hcl' },
+ { language: 'hcl', offset: 8, type: '' },
+ { language: 'hcl', offset: 9, type: 'string.hcl' },
+ { language: 'hcl', offset: 14, type: '' },
+ { language: 'hcl', offset: 15, type: 'string.hcl' },
+ { language: 'hcl', offset: 20, type: '' },
+ { language: 'hcl', offset: 21, type: 'delimiter.curly.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'foo = "bar"',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'operator.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'string.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'bar = 7',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'operator.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'number.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'baz = [1,2,3]',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'operator.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'delimiter.square.hcl' },
+ { language: 'hcl', offset: 7, type: 'number.hcl' },
+ { language: 'hcl', offset: 8, type: 'delimiter.hcl' },
+ { language: 'hcl', offset: 9, type: 'number.hcl' },
+ { language: 'hcl', offset: 10, type: 'delimiter.hcl' },
+ { language: 'hcl', offset: 11, type: 'number.hcl' },
+ { language: 'hcl', offset: 12, type: 'delimiter.square.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'foo = -12',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'operator.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'operator.hcl' },
+ { language: 'hcl', offset: 7, type: 'number.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'bar = 3.14159',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'operator.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'number.float.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'foo = true',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'operator.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'keyword.true.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'foo = false',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'operator.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'keyword.false.hcl' },
+ ],
+ ],
+ ],
+ [
+ // eslint-disable-next-line no-template-curly-in-string
+ 'bar = "${file("bing/bong.txt")}"',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'operator.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'string.hcl' },
+ { language: 'hcl', offset: 7, type: 'delimiter.hcl' },
+ { language: 'hcl', offset: 9, type: 'type.hcl' },
+ { language: 'hcl', offset: 13, type: 'delimiter.parenthesis.hcl' },
+ { language: 'hcl', offset: 14, type: 'string.hcl' },
+ { language: 'hcl', offset: 29, type: 'delimiter.parenthesis.hcl' },
+ { language: 'hcl', offset: 30, type: 'delimiter.hcl' },
+ { language: 'hcl', offset: 31, type: 'string.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'a = 1e-10',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 1, type: '' },
+ { language: 'hcl', offset: 2, type: 'operator.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'number.float.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'b = 1e+10',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 1, type: '' },
+ { language: 'hcl', offset: 2, type: 'operator.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'number.float.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'c = 1e10',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 1, type: '' },
+ { language: 'hcl', offset: 2, type: 'operator.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'number.float.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'd = 1.2e-10',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 1, type: '' },
+ { language: 'hcl', offset: 2, type: 'operator.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'number.float.hcl' },
+ ],
+ ],
+ ],
+ [
+ 'e = 1.2e+10',
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 1, type: '' },
+ { language: 'hcl', offset: 2, type: 'operator.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'number.float.hcl' },
+ ],
+ ],
+ ],
+ ])('%s', (string, tokens) => {
+ expect(editor.tokenize(string, 'hcl')).toEqual(tokens);
+ });
+});
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 974c0715c06..8f7fcc25cf0 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -291,6 +291,20 @@ describe('IDE store file actions', () => {
expect(store.state.openFiles[0].name).toBe(localFile.name);
});
});
+
+ it('does not toggle loading if toggleLoading=false', () => {
+ expect(localFile.loading).toBe(false);
+
+ return store
+ .dispatch('getFileData', {
+ path: localFile.path,
+ makeFileActive: false,
+ toggleLoading: false,
+ })
+ .then(() => {
+ expect(localFile.loading).toBe(true);
+ });
+ });
});
describe('Re-named success', () => {
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index e24f08fa802..5ae87f5f9cd 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -449,16 +449,16 @@ describe('IDE store getters', () => {
describe('getAvailableFileName', () => {
it.each`
path | newPath
- ${'foo'} | ${'foo_1'}
+ ${'foo'} | ${'foo-1'}
${'foo__93.png'} | ${'foo__94.png'}
- ${'foo/bar.png'} | ${'foo/bar_1.png'}
+ ${'foo/bar.png'} | ${'foo/bar-1.png'}
${'foo/bar--34.png'} | ${'foo/bar--35.png'}
${'foo/bar 2.png'} | ${'foo/bar 3.png'}
${'foo/bar-621.png'} | ${'foo/bar-622.png'}
- ${'jquery.min.js'} | ${'jquery_1.min.js'}
+ ${'jquery.min.js'} | ${'jquery-1.min.js'}
${'my_spec_22.js.snap'} | ${'my_spec_23.js.snap'}
- ${'subtitles5.mp4.srt'} | ${'subtitles_6.mp4.srt'}
- ${'sample_file.mp3'} | ${'sample_file_1.mp3'}
+ ${'subtitles5.mp4.srt'} | ${'subtitles-6.mp4.srt'}
+ ${'sample-file.mp3'} | ${'sample-file-1.mp3'}
${'Screenshot 2020-05-26 at 10.53.08 PM.png'} | ${'Screenshot 2020-05-26 at 11.53.08 PM.png'}
`('suffixes the path with a number if the path already exists', ({ path, newPath }) => {
localState.entries[path] = file();
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index babc50e54f1..cfe2bddf76c 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -76,59 +76,38 @@ describe('IDE commit module actions', () => {
.then(done)
.catch(done.fail);
});
+ });
- it('sets shouldCreateMR to true if "Create new MR" option is visible', done => {
- Object.assign(store.state, {
- shouldHideNewMrOption: false,
- });
+ describe('updateBranchName', () => {
+ let originalGon;
- testAction(
- actions.updateCommitAction,
- {},
- store.state,
- [
- {
- type: mutationTypes.UPDATE_COMMIT_ACTION,
- payload: { commitAction: expect.anything() },
- },
- { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: true },
- ],
- [],
- done,
- );
+ beforeEach(() => {
+ originalGon = window.gon;
+ window.gon = { current_username: 'johndoe' };
+
+ store.state.currentBranchId = 'master';
});
- it('sets shouldCreateMR to false if "Create new MR" option is hidden', done => {
- Object.assign(store.state, {
- shouldHideNewMrOption: true,
- });
+ afterEach(() => {
+ window.gon = originalGon;
+ });
- testAction(
- actions.updateCommitAction,
- {},
- store.state,
- [
- {
- type: mutationTypes.UPDATE_COMMIT_ACTION,
- payload: { commitAction: expect.anything() },
- },
- { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: false },
- ],
- [],
- done,
- );
+ it('updates store with new branch name', async () => {
+ await store.dispatch('commit/updateBranchName', 'branch-name');
+
+ expect(store.state.commit.newBranchName).toBe('branch-name');
});
});
- describe('updateBranchName', () => {
- it('updates store with new branch name', done => {
- store
- .dispatch('commit/updateBranchName', 'branch-name')
- .then(() => {
- expect(store.state.commit.newBranchName).toBe('branch-name');
- })
- .then(done)
- .catch(done.fail);
+ describe('addSuffixToBranchName', () => {
+ it('adds suffix to branchName', async () => {
+ jest.spyOn(Math, 'random').mockReturnValue(0.391352525);
+
+ store.state.commit.newBranchName = 'branch-name';
+
+ await store.dispatch('commit/addSuffixToBranchName');
+
+ expect(store.state.commit.newBranchName).toBe('branch-name-39135');
});
});
@@ -318,13 +297,16 @@ describe('IDE commit module actions', () => {
currentBranchId: 'master',
projects: {
abcproject: {
+ default_branch: 'master',
web_url: 'webUrl',
branches: {
master: {
+ name: 'master',
workingReference: '1',
commit: {
id: TEST_COMMIT_SHA,
},
+ can_push: true,
},
},
userPermissions: {
@@ -499,6 +481,16 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
+ it('does not redirect to merge request page if shouldCreateMR is checked, but branch is the default branch', async () => {
+ jest.spyOn(eventHub, '$on').mockImplementation();
+
+ store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ store.state.commit.shouldCreateMR = true;
+
+ await store.dispatch('commit/commitChanges');
+ expect(visitUrl).not.toHaveBeenCalled();
+ });
+
it('resets changed files before redirecting', () => {
jest.spyOn(eventHub, '$on').mockImplementation();
diff --git a/spec/frontend/ide/stores/mutations/file_spec.js b/spec/frontend/ide/stores/mutations/file_spec.js
index b53e40be980..d303de6e9ef 100644
--- a/spec/frontend/ide/stores/mutations/file_spec.js
+++ b/spec/frontend/ide/stores/mutations/file_spec.js
@@ -39,20 +39,34 @@ describe('IDE store file mutations', () => {
});
describe('TOGGLE_FILE_OPEN', () => {
- beforeEach(() => {
+ it('adds into opened files', () => {
mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
- });
- it('adds into opened files', () => {
expect(localFile.opened).toBeTruthy();
expect(localState.openFiles.length).toBe(1);
});
- it('removes from opened files', () => {
+ describe('if already open', () => {
+ it('removes from opened files', () => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
+ mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
+
+ expect(localFile.opened).toBeFalsy();
+ expect(localState.openFiles.length).toBe(0);
+ });
+ });
+
+ it.each`
+ entry | loading
+ ${{ opened: false }} | ${true}
+ ${{ opened: false, tempFile: true }} | ${false}
+ ${{ opened: true }} | ${false}
+ `('for state: $entry, sets loading=$loading', ({ entry, loading }) => {
+ Object.assign(localFile, entry);
+
mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
- expect(localFile.opened).toBeFalsy();
- expect(localState.openFiles.length).toBe(0);
+ expect(localFile.loading).toBe(loading);
});
});
diff --git a/spec/frontend/ide/stores/utils_spec.js b/spec/frontend/ide/stores/utils_spec.js
index d1eb4304c79..b185013050e 100644
--- a/spec/frontend/ide/stores/utils_spec.js
+++ b/spec/frontend/ide/stores/utils_spec.js
@@ -46,7 +46,7 @@ describe('Multi-file store utils', () => {
path: 'added',
tempFile: true,
content: 'new file content',
- rawPath: 'data:image/png;base64,abc',
+ rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b',
lastCommitSha: '123456789',
},
{ ...file('deletedFile'), path: 'deletedFile', deleted: true },
@@ -77,7 +77,8 @@ describe('Multi-file store utils', () => {
{
action: commitActionTypes.create,
file_path: 'added',
- content: 'new file content',
+ // atob("new file content")
+ content: 'bmV3IGZpbGUgY29udGVudA==',
encoding: 'base64',
last_commit_id: '123456789',
previous_path: undefined,
@@ -117,7 +118,7 @@ describe('Multi-file store utils', () => {
path: 'added',
tempFile: true,
content: 'new file content',
- rawPath: 'data:image/png;base64,abc',
+ rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b',
lastCommitSha: '123456789',
},
],
@@ -148,7 +149,8 @@ describe('Multi-file store utils', () => {
{
action: commitActionTypes.create,
file_path: 'added',
- content: 'new file content',
+ // atob("new file content")
+ content: 'bmV3IGZpbGUgY29udGVudA==',
encoding: 'base64',
last_commit_id: '123456789',
previous_path: undefined,
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index 97dc8217ecc..6cd2128d356 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -9,6 +9,7 @@ import {
getPathParents,
getPathParent,
readFileAsDataURL,
+ addNumericSuffix,
} from '~/ide/utils';
describe('WebIDE utils', () => {
@@ -291,4 +292,43 @@ describe('WebIDE utils', () => {
});
});
});
+
+ /*
+ * hello-2425 -> hello-2425
+ * hello.md -> hello-1.md
+ * hello_2.md -> hello_3.md
+ * hello_ -> hello_1
+ * master-patch-22432 -> master-patch-22433
+ * patch_332 -> patch_333
+ */
+
+ describe('addNumericSuffix', () => {
+ it.each`
+ input | output
+ ${'hello'} | ${'hello-1'}
+ ${'hello2'} | ${'hello-3'}
+ ${'hello.md'} | ${'hello-1.md'}
+ ${'hello_2.md'} | ${'hello_3.md'}
+ ${'hello_'} | ${'hello_1'}
+ ${'master-patch-22432'} | ${'master-patch-22433'}
+ ${'patch_332'} | ${'patch_333'}
+ `('adds a numeric suffix to a given filename/branch name: $input', ({ input, output }) => {
+ expect(addNumericSuffix(input)).toBe(output);
+ });
+
+ it.each`
+ input | output
+ ${'hello'} | ${'hello-39135'}
+ ${'hello2'} | ${'hello-39135'}
+ ${'hello.md'} | ${'hello-39135.md'}
+ ${'hello_2.md'} | ${'hello_39135.md'}
+ ${'hello_'} | ${'hello_39135'}
+ ${'master-patch-22432'} | ${'master-patch-39135'}
+ ${'patch_332'} | ${'patch_39135'}
+ `('adds a random suffix if randomize=true is passed for name: $input', ({ input, output }) => {
+ jest.spyOn(Math, 'random').mockReturnValue(0.391352525);
+
+ expect(addNumericSuffix(input, true)).toBe(output);
+ });
+ });
});