diff options
Diffstat (limited to 'spec/frontend/ide')
13 files changed, 392 insertions, 283 deletions
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js index b7349b8fed1..294f5eee863 100644 --- a/spec/frontend/ide/components/panes/right_spec.js +++ b/spec/frontend/ide/components/panes/right_spec.js @@ -3,16 +3,12 @@ import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue'; import RightPane from '~/ide/components/panes/right.vue'; -import SwitchEditorsView from '~/ide/components/switch_editors/switch_editors_view.vue'; import { rightSidebarViews } from '~/ide/constants'; import { createStore } from '~/ide/stores'; import extendStore from '~/ide/stores/extend'; -import { __ } from '~/locale'; Vue.use(Vuex); -const SWITCH_EDITORS_VIEW_NAME = 'switch-editors'; - describe('ide/components/panes/right.vue', () => { let wrapper; let store; @@ -45,7 +41,6 @@ describe('ide/components/panes/right.vue', () => { it('renders collapsible-sidebar', () => { expect(wrapper.findComponent(CollapsibleSidebar).props()).toMatchObject({ side: 'right', - initOpenView: SWITCH_EDITORS_VIEW_NAME, }); }); }); @@ -130,32 +125,4 @@ describe('ide/components/panes/right.vue', () => { ); }); }); - - describe('switch editors tab', () => { - beforeEach(() => { - createComponent(); - }); - - it.each` - desc | canUseNewWebIde | expectedShow - ${'is shown'} | ${true} | ${true} - ${'is not shown'} | ${false} | ${false} - `('with canUseNewWebIde=$canUseNewWebIde, $desc', async ({ canUseNewWebIde, expectedShow }) => { - Object.assign(store.state, { canUseNewWebIde }); - - await nextTick(); - - expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - show: expectedShow, - title: __('Switch editors'), - views: [ - { component: SwitchEditorsView, name: SWITCH_EDITORS_VIEW_NAME, keepAlive: true }, - ], - }), - ]), - ); - }); - }); }); diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js index 545924c9c11..d82b97561f0 100644 --- a/spec/frontend/ide/components/pipelines/list_spec.js +++ b/spec/frontend/ide/components/pipelines/list_spec.js @@ -185,7 +185,7 @@ describe('IDE pipelines list', () => { }, ); - expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:'); + expect(wrapper.text()).toContain('Unable to create pipeline'); expect(wrapper.text()).toContain(yamlError); }); }); diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index 9921d8cba18..211fee31a9c 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -4,13 +4,17 @@ import { editor as monacoEditor, Range } from 'monaco-editor'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { shallowMount } from '@vue/test-utils'; -import '~/behaviors/markdown/render_gfm'; import waitForPromises from 'helpers/wait_for_promises'; import { stubPerformanceWebAPI } from 'helpers/performance'; import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data'; -import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants'; +import { + EDITOR_CODE_INSTANCE_FN, + EDITOR_DIFF_INSTANCE_FN, + EXTENSION_CI_SCHEMA_FILE_NAME_MATCH, +} from '~/editor/constants'; import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext'; import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext'; +import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext'; import SourceEditor from '~/editor/source_editor'; import RepoEditor from '~/ide/components/repo_editor.vue'; import { leftSidebarViews, FILE_VIEW_MODE_PREVIEW, viewerTypes } from '~/ide/constants'; @@ -22,6 +26,9 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer import SourceEditorInstance from '~/editor/source_editor_instance'; import { file } from '../helpers'; +jest.mock('~/behaviors/markdown/render_gfm'); +jest.mock('~/editor/extensions/source_editor_ci_schema_ext'); + const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown'; const CURRENT_PROJECT_ID = 'gitlab-org/gitlab'; @@ -46,6 +53,12 @@ const dummyFile = { tempFile: true, active: true, }, + ciConfig: { + ...file(EXTENSION_CI_SCHEMA_FILE_NAME_MATCH), + content: '', + tempFile: true, + active: true, + }, empty: { ...file('empty'), tempFile: false, @@ -101,6 +114,7 @@ describe('RepoEditor', () => { let createDiffInstanceSpy; let createModelSpy; let applyExtensionSpy; + let removeExtensionSpy; let extensionsStore; const waitForEditorSetup = () => @@ -108,7 +122,7 @@ describe('RepoEditor', () => { vm.$once('editorSetup', resolve); }); - const createComponent = async ({ state = {}, activeFile = dummyFile.text } = {}) => { + const createComponent = async ({ state = {}, activeFile = dummyFile.text, flags = {} } = {}) => { const store = prepareStore(state, activeFile); wrapper = shallowMount(RepoEditor, { store, @@ -118,6 +132,9 @@ describe('RepoEditor', () => { mocks: { ContentViewer, }, + provide: { + glFeatures: flags, + }, }); await waitForPromises(); vm = wrapper.vm; @@ -137,6 +154,7 @@ describe('RepoEditor', () => { createDiffInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_DIFF_INSTANCE_FN); createModelSpy = jest.spyOn(monacoEditor, 'createModel'); applyExtensionSpy = jest.spyOn(SourceEditorInstance.prototype, 'use'); + removeExtensionSpy = jest.spyOn(SourceEditorInstance.prototype, 'unuse'); jest.spyOn(service, 'getFileData').mockResolvedValue(); jest.spyOn(service, 'getRawFileData').mockResolvedValue(); }); @@ -177,6 +195,76 @@ describe('RepoEditor', () => { }); }); + describe('schema registration for .gitlab-ci.yml', () => { + const setup = async (activeFile, flagIsOn = true) => { + await createComponent({ + flags: { + schemaLinting: flagIsOn, + }, + }); + vm.editor.registerCiSchema = jest.fn(); + if (activeFile) { + wrapper.setProps({ file: activeFile }); + } + await waitForPromises(); + await nextTick(); + }; + it.each` + flagIsOn | activeFile | shouldUseExtension | desc + ${false} | ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`} + ${true} | ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`} + ${false} | ${dummyFile.ciConfig} | ${false} | ${`file is CI config; should NOT`} + ${true} | ${dummyFile.ciConfig} | ${true} | ${`file is CI config; should`} + `( + 'when the flag is "$flagIsOn", $desc use extension', + async ({ flagIsOn, activeFile, shouldUseExtension }) => { + await setup(activeFile, flagIsOn); + + if (shouldUseExtension) { + expect(applyExtensionSpy).toHaveBeenCalledWith({ + definition: CiSchemaExtension, + }); + } else { + expect(applyExtensionSpy).not.toHaveBeenCalledWith({ + definition: CiSchemaExtension, + }); + } + }, + ); + it('stores the fetched extension and does not double-fetch the schema', async () => { + await setup(); + expect(CiSchemaExtension).toHaveBeenCalledTimes(0); + + wrapper.setProps({ file: dummyFile.ciConfig }); + await waitForPromises(); + await nextTick(); + expect(CiSchemaExtension).toHaveBeenCalledTimes(1); + expect(vm.CiSchemaExtension).toEqual(CiSchemaExtension); + expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(1); + + wrapper.setProps({ file: dummyFile.markdown }); + await waitForPromises(); + await nextTick(); + expect(CiSchemaExtension).toHaveBeenCalledTimes(1); + expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(1); + + wrapper.setProps({ file: dummyFile.ciConfig }); + await waitForPromises(); + await nextTick(); + expect(CiSchemaExtension).toHaveBeenCalledTimes(1); + expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(2); + }); + it('unuses the existing CI extension if the new model is not CI config', async () => { + await setup(dummyFile.ciConfig); + + expect(removeExtensionSpy).not.toHaveBeenCalled(); + wrapper.setProps({ file: dummyFile.markdown }); + await waitForPromises(); + await nextTick(); + expect(removeExtensionSpy).toHaveBeenCalledWith(CiSchemaExtension); + }); + }); + describe('when file is markdown', () => { let mock; let activeFile; diff --git a/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js b/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js deleted file mode 100644 index 7a958391fea..00000000000 --- a/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js +++ /dev/null @@ -1,214 +0,0 @@ -import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import waitForPromises from 'helpers/wait_for_promises'; -import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; -import { createAlert } from '~/flash'; -import axios from '~/lib/utils/axios_utils'; -import { logError } from '~/lib/logger'; -import { __ } from '~/locale'; -import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; -import SwitchEditorsView, { - MSG_ERROR_ALERT, - MSG_CONFIRM, - MSG_TITLE, - MSG_LEARN_MORE, - MSG_DESCRIPTION, -} from '~/ide/components/switch_editors/switch_editors_view.vue'; -import eventHub from '~/ide/eventhub'; -import { createStore } from '~/ide/stores'; - -jest.mock('~/flash'); -jest.mock('~/lib/logger'); -jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); - -const TEST_USER_PREFERENCES_PATH = '/test/user-pref/path'; -const TEST_SWITCH_EDITOR_SVG_PATH = '/test/switch/editor/path.svg'; -const TEST_HREF = '/test/new/web/ide/href'; - -describe('~/ide/components/switch_editors/switch_editors_view.vue', () => { - useMockLocationHelper(); - - let store; - let wrapper; - let confirmResolve; - let requestSpy; - let skipBeforeunloadSpy; - let axiosMock; - - // region: finders ------------------ - const findButton = () => wrapper.findComponent(GlButton); - const findEmptyState = () => wrapper.findComponent(GlEmptyState); - - // region: actions ------------------ - const triggerSwitchPreference = () => findButton().vm.$emit('click'); - const submitConfirm = async (val) => { - confirmResolve(val); - - // why: We need to wait for promises for the immediate next lines to be executed - await waitForPromises(); - }; - - const createComponent = () => { - wrapper = shallowMount(SwitchEditorsView, { - store, - stubs: { - GlEmptyState, - }, - }); - }; - - // region: test setup ------------------ - beforeEach(() => { - // Setup skip-beforeunload side-effect - skipBeforeunloadSpy = jest.fn(); - eventHub.$on('skip-beforeunload', skipBeforeunloadSpy); - - // Setup request side-effect - requestSpy = jest.fn().mockImplementation(() => new Promise(() => {})); - axiosMock = new MockAdapter(axios); - axiosMock.onPut(TEST_USER_PREFERENCES_PATH).reply(({ data }) => requestSpy(data)); - - // Setup store - store = createStore(); - store.state.userPreferencesPath = TEST_USER_PREFERENCES_PATH; - store.state.switchEditorSvgPath = TEST_SWITCH_EDITOR_SVG_PATH; - store.state.links = { - newWebIDEHelpPagePath: TEST_HREF, - }; - - // Setup user confirm side-effect - confirmAction.mockImplementation( - () => - new Promise((resolve) => { - confirmResolve = resolve; - }), - ); - }); - - afterEach(() => { - eventHub.$off('skip-beforeunload', skipBeforeunloadSpy); - - axiosMock.restore(); - }); - - // region: tests ------------------ - describe('default', () => { - beforeEach(() => { - createComponent(); - }); - - it('render empty state', () => { - expect(findEmptyState().props()).toMatchObject({ - svgPath: TEST_SWITCH_EDITOR_SVG_PATH, - svgHeight: 150, - title: MSG_TITLE, - }); - }); - - it('render link', () => { - expect(wrapper.findComponent(GlLink).attributes('href')).toBe(TEST_HREF); - expect(wrapper.findComponent(GlLink).text()).toBe(MSG_LEARN_MORE); - }); - - it('renders description', () => { - expect(findEmptyState().text()).toContain(MSG_DESCRIPTION); - }); - - it('is not loading', () => { - expect(findButton().props('loading')).toBe(false); - }); - }); - - describe('when user triggers switch preference', () => { - beforeEach(() => { - createComponent(); - - triggerSwitchPreference(); - }); - - it('creates a single confirm', () => { - // Call again to ensure that we only show 1 confirm action - triggerSwitchPreference(); - - expect(confirmAction).toHaveBeenCalledTimes(1); - expect(confirmAction).toHaveBeenCalledWith(MSG_CONFIRM, { - primaryBtnText: __('Switch editors'), - cancelBtnText: __('Cancel'), - }); - }); - - it('starts loading', () => { - expect(findButton().props('loading')).toBe(true); - }); - - describe('when user cancels confirm', () => { - beforeEach(async () => { - await submitConfirm(false); - }); - - it('does not make request', () => { - expect(requestSpy).not.toHaveBeenCalled(); - }); - - it('can be triggered again', () => { - triggerSwitchPreference(); - - expect(confirmAction).toHaveBeenCalledTimes(2); - }); - }); - - describe('when user accepts confirm and response success', () => { - beforeEach(async () => { - requestSpy.mockReturnValue([200, {}]); - await submitConfirm(true); - }); - - it('does not handle error', () => { - expect(logError).not.toHaveBeenCalled(); - expect(createAlert).not.toHaveBeenCalled(); - }); - - it('emits "skip-beforeunload" and reloads', () => { - expect(skipBeforeunloadSpy).toHaveBeenCalledTimes(1); - expect(window.location.reload).toHaveBeenCalledTimes(1); - }); - - it('calls request', () => { - expect(requestSpy).toHaveBeenCalledTimes(1); - expect(requestSpy).toHaveBeenCalledWith( - JSON.stringify({ user: { use_legacy_web_ide: false } }), - ); - }); - - it('is not loading', () => { - expect(findButton().props('loading')).toBe(false); - }); - }); - - describe('when user accepts confirm and response fails', () => { - beforeEach(async () => { - requestSpy.mockReturnValue([400, {}]); - await submitConfirm(true); - }); - - it('handles error', () => { - expect(logError).toHaveBeenCalledTimes(1); - expect(logError).toHaveBeenCalledWith( - 'Error while updating user preferences', - expect.any(Error), - ); - - expect(createAlert).toHaveBeenCalledTimes(1); - expect(createAlert).toHaveBeenCalledWith({ - message: MSG_ERROR_ALERT, - }); - }); - - it('does not reload', () => { - expect(skipBeforeunloadSpy).not.toHaveBeenCalled(); - expect(window.location.reload).not.toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js index 067da25cb52..97254ab680b 100644 --- a/spec/frontend/ide/init_gitlab_web_ide_spec.js +++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js @@ -1,62 +1,190 @@ import { start } from '@gitlab/web-ide'; +import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from '~/ide/constants'; import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action'; +import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form'; import { TEST_HOST } from 'helpers/test_constants'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import waitForPromises from 'helpers/wait_for_promises'; jest.mock('@gitlab/web-ide'); +jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_action'); +jest.mock('~/lib/utils/create_and_submit_form'); +jest.mock('~/lib/utils/csrf', () => ({ + token: 'mock-csrf-token', + headerKey: 'mock-csrf-header', +})); const ROOT_ELEMENT_ID = 'ide'; const TEST_NONCE = 'test123nonce'; const TEST_PROJECT_PATH = 'group1/project1'; const TEST_BRANCH_NAME = '12345-foo-patch'; const TEST_GITLAB_URL = 'https://test-gitlab/'; +const TEST_USER_PREFERENCES_PATH = '/user/preferences'; const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path'; +const TEST_FILE_PATH = 'foo/README.md'; +const TEST_MR_ID = '7'; +const TEST_MR_TARGET_PROJECT = 'gitlab-org/the-real-gitlab'; +const TEST_FORK_INFO = { fork_path: '/forky' }; +const TEST_IDE_REMOTE_PATH = '/-/ide/remote/:remote_host/:remote_path'; +const TEST_START_REMOTE_PARAMS = { + remoteHost: 'dev.example.gitlab.com/test', + remotePath: '/test/projects/f oo', + connectionToken: '123abc', +}; describe('ide/init_gitlab_web_ide', () => { + let resolveConfirm; + const createRootElement = () => { const el = document.createElement('div'); el.id = ROOT_ELEMENT_ID; // why: We'll test that this class is removed later - el.classList.add('ide-loading'); + el.classList.add('test-class'); el.dataset.projectPath = TEST_PROJECT_PATH; el.dataset.cspNonce = TEST_NONCE; el.dataset.branchName = TEST_BRANCH_NAME; + el.dataset.ideRemotePath = TEST_IDE_REMOTE_PATH; + el.dataset.userPreferencesPath = TEST_USER_PREFERENCES_PATH; + el.dataset.mergeRequest = TEST_MR_ID; + el.dataset.filePath = TEST_FILE_PATH; document.body.append(el); }; const findRootElement = () => document.getElementById(ROOT_ELEMENT_ID); - const act = () => initGitlabWebIDE(findRootElement()); + const createSubject = () => initGitlabWebIDE(findRootElement()); + const triggerHandleStartRemote = (startRemoteParams) => { + const [, config] = start.mock.calls[0]; + + config.handleStartRemote(startRemoteParams); + }; beforeEach(() => { process.env.GITLAB_WEB_IDE_PUBLIC_PATH = TEST_GITLAB_WEB_IDE_PUBLIC_PATH; window.gon.gitlab_url = TEST_GITLAB_URL; - createRootElement(); + confirmAction.mockImplementation( + () => + new Promise((resolve) => { + resolveConfirm = resolve; + }), + ); - act(); + createRootElement(); }); afterEach(() => { document.body.innerHTML = ''; }); - it('calls start with element', () => { - expect(start).toHaveBeenCalledWith(findRootElement(), { - baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`, - projectPath: TEST_PROJECT_PATH, - ref: TEST_BRANCH_NAME, - gitlabUrl: TEST_GITLAB_URL, - nonce: TEST_NONCE, + describe('default', () => { + beforeEach(() => { + createSubject(); + }); + + it('calls start with element', () => { + expect(start).toHaveBeenCalledTimes(1); + expect(start).toHaveBeenCalledWith(findRootElement(), { + baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`, + projectPath: TEST_PROJECT_PATH, + ref: TEST_BRANCH_NAME, + filePath: TEST_FILE_PATH, + mrId: TEST_MR_ID, + mrTargetProject: '', + forkInfo: null, + gitlabUrl: TEST_GITLAB_URL, + nonce: TEST_NONCE, + httpHeaders: { + 'mock-csrf-header': 'mock-csrf-token', + 'X-Requested-With': 'XMLHttpRequest', + }, + links: { + userPreferences: TEST_USER_PREFERENCES_PATH, + feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE, + }, + handleStartRemote: expect.any(Function), + }); + }); + + it('clears classes and data from root element', () => { + const rootEl = findRootElement(); + + // why: Snapshot to test that the element was cleaned including `test-class` + expect(rootEl.outerHTML).toBe( + '<div id="ide" class="gl--flex-center gl-relative gl-h-full"></div>', + ); + }); + + describe('when handleStartRemote is triggered', () => { + beforeEach(() => { + triggerHandleStartRemote(TEST_START_REMOTE_PARAMS); + }); + + it('promts for confirm', () => { + expect(confirmAction).toHaveBeenCalledWith(expect.any(String), { + primaryBtnText: expect.any(String), + cancelBtnText: expect.any(String), + }); + }); + + it('does not submit, when not confirmed', async () => { + resolveConfirm(false); + + await waitForPromises(); + + expect(createAndSubmitForm).not.toHaveBeenCalled(); + }); + + it('submits, when confirmed', async () => { + resolveConfirm(true); + + await waitForPromises(); + + expect(createAndSubmitForm).toHaveBeenCalledWith({ + url: '/-/ide/remote/dev.example.gitlab.com%2Ftest/test/projects/f%20oo', + data: { + connection_token: TEST_START_REMOTE_PARAMS.connectionToken, + return_url: window.location.href, + }, + }); + }); + }); + }); + + describe('when URL has target_project in query params', () => { + beforeEach(() => { + setWindowLocation( + `https://example.com/-/ide?target_project=${encodeURIComponent(TEST_MR_TARGET_PROJECT)}`, + ); + + createSubject(); + }); + + it('includes mrTargetProject', () => { + expect(start).toHaveBeenCalledWith( + findRootElement(), + expect.objectContaining({ + mrTargetProject: TEST_MR_TARGET_PROJECT, + }), + ); }); }); - it('clears classes and data from root element', () => { - const rootEl = findRootElement(); + describe('when forkInfo is in dataset', () => { + beforeEach(() => { + findRootElement().dataset.forkInfo = JSON.stringify(TEST_FORK_INFO); - // why: Snapshot to test that `ide-loading` was removed and no other - // artifacts are remaining. - expect(rootEl.outerHTML).toBe( - '<div id="ide" class="gl--flex-center gl-relative gl-h-full"></div>', - ); + createSubject(); + }); + + it('includes forkInfo', () => { + expect(start).toHaveBeenCalledWith( + findRootElement(), + expect.objectContaining({ + forkInfo: TEST_FORK_INFO, + }), + ); + }); }); }); diff --git a/spec/frontend/ide/lib/common/model_spec.js b/spec/frontend/ide/lib/common/model_spec.js index 5d1623429c0..39c50f628c2 100644 --- a/spec/frontend/ide/lib/common/model_spec.js +++ b/spec/frontend/ide/lib/common/model_spec.js @@ -149,7 +149,6 @@ describe('Multi-file editor library model', () => { model.updateOptions({ insertSpaces: true, someOption: 'some value' }); expect(model.options).toEqual({ - endOfLine: 0, insertFinalNewline: true, insertSpaces: true, someOption: 'some value', @@ -181,16 +180,12 @@ describe('Multi-file editor library model', () => { describe('applyCustomOptions', () => { it.each` option | value | contentBefore | contentAfter - ${'endOfLine'} | ${0} | ${'hello\nworld\n'} | ${'hello\nworld\n'} - ${'endOfLine'} | ${0} | ${'hello\r\nworld\r\n'} | ${'hello\nworld\n'} - ${'endOfLine'} | ${1} | ${'hello\nworld\n'} | ${'hello\r\nworld\r\n'} - ${'endOfLine'} | ${1} | ${'hello\r\nworld\r\n'} | ${'hello\r\nworld\r\n'} ${'insertFinalNewline'} | ${true} | ${'hello\nworld'} | ${'hello\nworld\n'} ${'insertFinalNewline'} | ${true} | ${'hello\nworld\n'} | ${'hello\nworld\n'} ${'insertFinalNewline'} | ${false} | ${'hello\nworld'} | ${'hello\nworld'} ${'trimTrailingWhitespace'} | ${true} | ${'hello \t\nworld \t\n'} | ${'hello\nworld\n'} - ${'trimTrailingWhitespace'} | ${true} | ${'hello \t\r\nworld \t\r\n'} | ${'hello\nworld\n'} - ${'trimTrailingWhitespace'} | ${false} | ${'hello \t\r\nworld \t\r\n'} | ${'hello \t\nworld \t\n'} + ${'trimTrailingWhitespace'} | ${true} | ${'hello \t\r\nworld \t\r\n'} | ${'hello\r\nworld\r\n'} + ${'trimTrailingWhitespace'} | ${false} | ${'hello \t\r\nworld \t\r\n'} | ${'hello \t\r\nworld \t\r\n'} `( 'correctly applies custom option $option=$value to content', ({ option, value, contentBefore, contentAfter }) => { diff --git a/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js new file mode 100644 index 00000000000..4b4e96f3b41 --- /dev/null +++ b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js @@ -0,0 +1,22 @@ +import { getBaseConfig } from '~/ide/lib/gitlab_web_ide/get_base_config'; +import { TEST_HOST } from 'helpers/test_constants'; + +const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/gitlab-web-ide/public/path'; +const TEST_GITLAB_URL = 'https://gdk.test/'; + +describe('~/ide/lib/gitlab_web_ide/get_base_config', () => { + it('returns base properties for @gitlab/web-ide config', () => { + // why: add trailing "/" to test that it gets removed + process.env.GITLAB_WEB_IDE_PUBLIC_PATH = `${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}/`; + window.gon.gitlab_url = TEST_GITLAB_URL; + + // act + const actual = getBaseConfig(); + + // asset + expect(actual).toEqual({ + baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`, + gitlabUrl: TEST_GITLAB_URL, + }); + }); +}); diff --git a/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js new file mode 100644 index 00000000000..35cf41b31f5 --- /dev/null +++ b/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js @@ -0,0 +1,32 @@ +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { setupRootElement } from '~/ide/lib/gitlab_web_ide/setup_root_element'; + +describe('~/ide/lib/gitlab_web_ide/setup_root_element', () => { + beforeEach(() => { + setHTMLFixture(` + <div id="ide-test-root" class="js-not-a-real-class"> + <span>We are loading lots of stuff...</span> + </div> + `); + }); + + afterEach(() => { + resetHTMLFixture(); + }); + + const findIDERoot = () => document.getElementById('ide-test-root'); + + it('has no children, has original ID, and classes', () => { + const result = setupRootElement(findIDERoot()); + + // why: Assert that the return element matches the new one found in the dom + // (implying a el.replaceWith...) + expect(result).toBe(findIDERoot()); + expect(result).toMatchInlineSnapshot(` + <div + class="gl--flex-center gl-relative gl-h-full" + id="ide-test-root" + /> + `); + }); +}); diff --git a/spec/frontend/ide/remote/index_spec.js b/spec/frontend/ide/remote/index_spec.js new file mode 100644 index 00000000000..0f23b0a4e45 --- /dev/null +++ b/spec/frontend/ide/remote/index_spec.js @@ -0,0 +1,91 @@ +import { startRemote } from '@gitlab/web-ide'; +import { getBaseConfig, setupRootElement } from '~/ide/lib/gitlab_web_ide'; +import { mountRemoteIDE } from '~/ide/remote'; +import { TEST_HOST } from 'helpers/test_constants'; +import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; + +jest.mock('@gitlab/web-ide'); +jest.mock('~/ide/lib/gitlab_web_ide'); + +const TEST_DATA = { + remoteHost: 'example.com:3443', + remotePath: 'test/path/gitlab', + cspNonce: 'just7some8noncense', + connectionToken: 'connectAtoken', + returnUrl: 'https://example.com/return', +}; + +const TEST_BASE_CONFIG = { + gitlabUrl: '/test/gitlab', +}; + +const TEST_RETURN_URL_SAME_ORIGIN = `${TEST_HOST}/foo/example`; + +describe('~/ide/remote/index', () => { + useMockLocationHelper(); + const originalHref = window.location.href; + + let el; + let rootEl; + + beforeEach(() => { + el = document.createElement('div'); + Object.entries(TEST_DATA).forEach(([key, value]) => { + el.dataset[key] = value; + }); + + // Stub setupRootElement so we can assert on return element + rootEl = document.createElement('div'); + setupRootElement.mockReturnValue(rootEl); + + // Stub getBaseConfig so we can assert + getBaseConfig.mockReturnValue(TEST_BASE_CONFIG); + }); + + describe('default', () => { + beforeEach(() => { + mountRemoteIDE(el); + }); + + it('calls startRemote', () => { + expect(startRemote).toHaveBeenCalledWith(rootEl, { + ...TEST_BASE_CONFIG, + nonce: TEST_DATA.cspNonce, + connectionToken: TEST_DATA.connectionToken, + remoteAuthority: `/${TEST_DATA.remoteHost}`, + hostPath: `/${TEST_DATA.remotePath}`, + handleError: expect.any(Function), + handleClose: expect.any(Function), + }); + }); + }); + + describe.each` + returnUrl | fnName | reloadExpectation | hrefExpectation + ${TEST_DATA.returnUrl} | ${'handleError'} | ${1} | ${originalHref} + ${TEST_DATA.returnUrl} | ${'handleClose'} | ${1} | ${originalHref} + ${TEST_RETURN_URL_SAME_ORIGIN} | ${'handleClose'} | ${0} | ${TEST_RETURN_URL_SAME_ORIGIN} + ${TEST_RETURN_URL_SAME_ORIGIN} | ${'handleError'} | ${0} | ${TEST_RETURN_URL_SAME_ORIGIN} + ${''} | ${'handleClose'} | ${1} | ${originalHref} + `( + 'with returnUrl=$returnUrl and fn=$fnName', + ({ returnUrl, fnName, reloadExpectation, hrefExpectation }) => { + beforeEach(() => { + el.dataset.returnUrl = returnUrl; + + mountRemoteIDE(el); + }); + + it('changes location', () => { + expect(window.location.reload).not.toHaveBeenCalled(); + + const [, config] = startRemote.mock.calls[0]; + + config[fnName](); + + expect(window.location.reload).toHaveBeenCalledTimes(reloadExpectation); + expect(window.location.href).toBe(hrefExpectation); + }); + }, + ); +}); diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js index 0fab828dfb3..5847e8e1518 100644 --- a/spec/frontend/ide/services/index_spec.js +++ b/spec/frontend/ide/services/index_spec.js @@ -6,7 +6,7 @@ import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout. import services from '~/ide/services'; import { query, mutate } from '~/ide/services/gql'; import { escapeFileUrl } from '~/lib/utils/url_utility'; -import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.query.graphql'; +import ciConfig from '~/ci/pipeline_editor/graphql/queries/ci_config.query.graphql'; import { projectData } from '../mock_data'; jest.mock('~/api'); diff --git a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js index fc00bd075e7..8d21088bcaf 100644 --- a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js +++ b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js @@ -10,7 +10,7 @@ import { import * as messages from '~/ide/stores/modules/terminal/messages'; import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types'; import axios from '~/lib/utils/axios_utils'; -import httpStatus from '~/lib/utils/http_status'; +import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status'; const TEST_PROJECT_PATH = 'lorem/root'; const TEST_BRANCH_ID = 'main'; @@ -78,7 +78,7 @@ describe('IDE store terminal check actions', () => { describe('receiveConfigCheckError', () => { it('handles error response', () => { - const status = httpStatus.UNPROCESSABLE_ENTITY; + const status = HTTP_STATUS_UNPROCESSABLE_ENTITY; const payload = { response: { status } }; return testAction( diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js index f48797415df..df365442c67 100644 --- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js +++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js @@ -6,7 +6,7 @@ import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/termi import * as messages from '~/ide/stores/modules/terminal/messages'; import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types'; import axios from '~/lib/utils/axios_utils'; -import httpStatus from '~/lib/utils/http_status'; +import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status'; jest.mock('~/flash'); @@ -285,7 +285,7 @@ describe('IDE store terminal session controls actions', () => { ); }); - [httpStatus.NOT_FOUND, httpStatus.UNPROCESSABLE_ENTITY].forEach((status) => { + [httpStatus.NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY].forEach((status) => { it(`dispatches request and startSession on ${status}`, () => { mock .onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' }) diff --git a/spec/frontend/ide/stores/modules/terminal/messages_spec.js b/spec/frontend/ide/stores/modules/terminal/messages_spec.js index e8f375a70b5..2a802d6b4af 100644 --- a/spec/frontend/ide/stores/modules/terminal/messages_spec.js +++ b/spec/frontend/ide/stores/modules/terminal/messages_spec.js @@ -1,7 +1,7 @@ import { escape } from 'lodash'; import { TEST_HOST } from 'spec/test_constants'; import * as messages from '~/ide/stores/modules/terminal/messages'; -import httpStatus from '~/lib/utils/http_status'; +import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status'; import { sprintf } from '~/locale'; const TEST_HELP_URL = `${TEST_HOST}/help`; @@ -9,7 +9,7 @@ const TEST_HELP_URL = `${TEST_HOST}/help`; describe('IDE store terminal messages', () => { describe('configCheckError', () => { it('returns job error, with status UNPROCESSABLE_ENTITY', () => { - const result = messages.configCheckError(httpStatus.UNPROCESSABLE_ENTITY, TEST_HELP_URL); + const result = messages.configCheckError(HTTP_STATUS_UNPROCESSABLE_ENTITY, TEST_HELP_URL); expect(result).toBe( sprintf( |