summaryrefslogtreecommitdiff
path: root/spec/frontend_integration/ide
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend_integration/ide')
-rw-r--r--spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap114
-rw-r--r--spec/frontend_integration/ide/helpers/ide_helper.js (renamed from spec/frontend_integration/ide/ide_helper.js)71
-rw-r--r--spec/frontend_integration/ide/helpers/mock_data.js12
-rw-r--r--spec/frontend_integration/ide/helpers/start.js17
-rw-r--r--spec/frontend_integration/ide/ide_integration_spec.js136
-rw-r--r--spec/frontend_integration/ide/user_opens_file_spec.js89
-rw-r--r--spec/frontend_integration/ide/user_opens_ide_spec.js160
7 files changed, 435 insertions, 164 deletions
diff --git a/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap b/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap
deleted file mode 100644
index 877cc78a111..00000000000
--- a/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap
+++ /dev/null
@@ -1,114 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`WebIDE runs 1`] = `
-<div>
- <article
- class="ide position-relative d-flex flex-column align-items-stretch"
- >
- <div
- class="ide-view flex-grow d-flex"
- >
- <div
- class="gl-relative multi-file-commit-panel flex-column"
- style="width: 340px;"
- >
- <div
- class="multi-file-commit-panel-inner"
- data-testid="ide-side-bar-inner"
- >
- <div
- class="multi-file-loading-container"
- >
- <div
- class="animation-container"
- >
- <div
- class="skeleton-line-1"
- />
- <div
- class="skeleton-line-2"
- />
- <div
- class="skeleton-line-3"
- />
- </div>
- </div>
- <div
- class="multi-file-loading-container"
- >
- <div
- class="animation-container"
- >
- <div
- class="skeleton-line-1"
- />
- <div
- class="skeleton-line-2"
- />
- <div
- class="skeleton-line-3"
- />
- </div>
- </div>
- <div
- class="multi-file-loading-container"
- >
- <div
- class="animation-container"
- >
- <div
- class="skeleton-line-1"
- />
- <div
- class="skeleton-line-2"
- />
- <div
- class="skeleton-line-3"
- />
- </div>
- </div>
- </div>
- <div
- class="position-absolute position-top-0 position-bottom-0 drag-handle position-right-0"
- size="340"
- style="cursor: ew-resize;"
- />
- </div>
- <div
- class="multi-file-edit-pane"
- >
- <div
- class="ide-empty-state"
- >
- <div
- class="row js-empty-state"
- >
- <div
- class="col-12"
- >
- <div
- class="svg-content svg-250"
- >
- <img
- src="/test/empty_state.svg"
- />
- </div>
- </div>
- <div
- class="col-12"
- >
- <div
- class="text-content text-center"
- >
- <h4>
- Make and review changes in the browser with the Web IDE
- </h4>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </article>
-</div>
-`;
diff --git a/spec/frontend_integration/ide/ide_helper.js b/spec/frontend_integration/ide/helpers/ide_helper.js
index fea8bc24031..fe8d5f93794 100644
--- a/spec/frontend_integration/ide/ide_helper.js
+++ b/spec/frontend_integration/ide/helpers/ide_helper.js
@@ -1,10 +1,18 @@
-import { findAllByText, fireEvent, getByLabelText, screen } from '@testing-library/dom';
+import {
+ findAllByText,
+ fireEvent,
+ getByLabelText,
+ findByTestId,
+ getByText,
+ screen,
+ findByText,
+} from '@testing-library/dom';
const isFolderRowOpen = row => row.matches('.folder.is-open');
const getLeftSidebar = () => screen.getByTestId('left-sidebar');
-const clickOnLeftSidebarTab = name => {
+export const switchLeftSidebarTab = name => {
const sidebar = getLeftSidebar();
const button = getByLabelText(sidebar, name);
@@ -12,16 +20,31 @@ const clickOnLeftSidebarTab = name => {
button.click();
};
-const findMonacoEditor = () =>
- screen.findByLabelText(/Editor content;/).then(x => x.closest('.monaco-editor'));
+export const getStatusBar = () => document.querySelector('.ide-status-bar');
-const findAndSetEditorValue = async value => {
+export const waitForMonacoEditor = () =>
+ new Promise(resolve => window.monaco.editor.onDidCreateEditor(resolve));
+
+export const findMonacoEditor = () =>
+ screen.findAllByLabelText(/Editor content;/).then(([x]) => x.closest('.monaco-editor'));
+
+export const findMonacoDiffEditor = () =>
+ screen.findAllByLabelText(/Editor content;/).then(([x]) => x.closest('.monaco-diff-editor'));
+
+export const findAndSetEditorValue = async value => {
const editor = await findMonacoEditor();
const uri = editor.getAttribute('data-uri');
window.monaco.editor.getModel(uri).setValue(value);
};
+export const getEditorValue = async () => {
+ const editor = await findMonacoEditor();
+ const uri = editor.getAttribute('data-uri');
+
+ return window.monaco.editor.getModel(uri).getValue();
+};
+
const findTreeBody = () => screen.findByTestId('ide-tree-body', {}, { timeout: 5000 });
const findRootActions = () => screen.findByTestId('ide-root-actions', {}, { timeout: 7000 });
@@ -68,11 +91,13 @@ const clickFileRowAction = (row, name) => {
dropdownAction.click();
};
-const findAndSetFileName = async value => {
- const nameField = await screen.findByTestId('file-name-field');
+const fillFileNameModal = async (value, submitText = 'Create file') => {
+ const modal = await screen.findByTestId('ide-new-entry');
+
+ const nameField = await findByTestId(modal, 'file-name-field');
fireEvent.input(nameField, { target: { value } });
- const createButton = screen.getByText('Create file');
+ const createButton = getByText(modal, submitText, { selector: 'button' });
createButton.click();
};
@@ -83,12 +108,19 @@ const findAndClickRootAction = async name => {
button.click();
};
+export const clickPreviewMarkdown = () => {
+ screen.getByText('Preview Markdown').click();
+};
+
export const openFile = async path => {
const row = await findAndTraverseToPath(path);
openFileRow(row);
};
+export const waitForTabToOpen = fileName =>
+ findByText(document.querySelector('.multi-file-edit-pane'), fileName);
+
export const createFile = async (path, content) => {
const parentPath = path
.split('/')
@@ -103,17 +135,36 @@ export const createFile = async (path, content) => {
await findAndClickRootAction('New file');
}
- await findAndSetFileName(path);
+ await fillFileNameModal(path);
await findAndSetEditorValue(content);
};
+export const getFilesList = () => {
+ return screen.getAllByTestId('file-row-name-container').map(e => e.textContent.trim());
+};
+
export const deleteFile = async path => {
const row = await findAndTraverseToPath(path);
clickFileRowAction(row, 'Delete');
};
+export const renameFile = async (path, newPath) => {
+ const row = await findAndTraverseToPath(path);
+ clickFileRowAction(row, 'Rename/Move');
+
+ await fillFileNameModal(newPath, 'Rename file');
+};
+
+export const closeFile = async path => {
+ const button = await screen.getByLabelText(`Close ${path}`, {
+ selector: '.multi-file-tabs button',
+ });
+
+ button.click();
+};
+
export const commit = async () => {
- clickOnLeftSidebarTab('Commit');
+ switchLeftSidebarTab('Commit');
screen.getByTestId('begin-commit-button').click();
await screen.findByLabelText(/Commit to .+ branch/).then(x => x.click());
diff --git a/spec/frontend_integration/ide/helpers/mock_data.js b/spec/frontend_integration/ide/helpers/mock_data.js
new file mode 100644
index 00000000000..f70739e5ac0
--- /dev/null
+++ b/spec/frontend_integration/ide/helpers/mock_data.js
@@ -0,0 +1,12 @@
+export const IDE_DATASET = {
+ emptyStateSvgPath: '/test/empty_state.svg',
+ noChangesStateSvgPath: '/test/no_changes_state.svg',
+ committedStateSvgPath: '/test/committed_state.svg',
+ pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg',
+ promotionSvgPath: '/test/promotion.svg',
+ ciHelpPagePath: '/test/ci_help_page',
+ webIDEHelpPagePath: '/test/web_ide_help_page',
+ clientsidePreviewEnabled: 'true',
+ renderWhitespaceInCode: 'false',
+ codesandboxBundlerUrl: 'test/codesandbox_bundler',
+};
diff --git a/spec/frontend_integration/ide/helpers/start.js b/spec/frontend_integration/ide/helpers/start.js
new file mode 100644
index 00000000000..9dc9649e1bf
--- /dev/null
+++ b/spec/frontend_integration/ide/helpers/start.js
@@ -0,0 +1,17 @@
+import { TEST_HOST } from 'helpers/test_constants';
+import extendStore from '~/ide/stores/extend';
+import { IDE_DATASET } from './mock_data';
+import { initIde } from '~/ide';
+
+export default (container, { isRepoEmpty = false, path = '' } = {}) => {
+ global.jsdom.reconfigure({
+ url: `${TEST_HOST}/-/ide/project/gitlab-test/lorem-ipsum${
+ isRepoEmpty ? '-empty' : ''
+ }/tree/master/-/${path}`,
+ });
+
+ const el = document.createElement('div');
+ Object.assign(el.dataset, IDE_DATASET);
+ container.appendChild(el);
+ return initIde(el, { extendStore });
+};
diff --git a/spec/frontend_integration/ide/ide_integration_spec.js b/spec/frontend_integration/ide/ide_integration_spec.js
index 1f5c1d38450..dacc538d5ba 100644
--- a/spec/frontend_integration/ide/ide_integration_spec.js
+++ b/spec/frontend_integration/ide/ide_integration_spec.js
@@ -1,61 +1,28 @@
-import { TEST_HOST } from 'helpers/test_constants';
import { waitForText } from 'helpers/wait_for_text';
import waitForPromises from 'helpers/wait_for_promises';
import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import { createCommitId } from 'test_helpers/factories/commit_id';
-import { initIde } from '~/ide';
-import extendStore from '~/ide/stores/extend';
-import * as ideHelper from './ide_helper';
-
-const TEST_DATASET = {
- emptyStateSvgPath: '/test/empty_state.svg',
- noChangesStateSvgPath: '/test/no_changes_state.svg',
- committedStateSvgPath: '/test/committed_state.svg',
- pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg',
- promotionSvgPath: '/test/promotion.svg',
- ciHelpPagePath: '/test/ci_help_page',
- webIDEHelpPagePath: '/test/web_ide_help_page',
- clientsidePreviewEnabled: 'true',
- renderWhitespaceInCode: 'false',
- codesandboxBundlerUrl: 'test/codesandbox_bundler',
-};
+import * as ideHelper from './helpers/ide_helper';
+import startWebIDE from './helpers/start';
describe('WebIDE', () => {
useOverclockTimers();
let vm;
- let root;
+ let container;
beforeEach(() => {
- root = document.createElement('div');
- document.body.appendChild(root);
-
- global.jsdom.reconfigure({
- url: `${TEST_HOST}/-/ide/project/gitlab-test/lorem-ipsum`,
- });
+ setFixtures('<div class="webide-container"></div>');
+ container = document.querySelector('.webide-container');
});
afterEach(() => {
vm.$destroy();
vm = null;
- root.remove();
- });
-
- const createComponent = () => {
- const el = document.createElement('div');
- Object.assign(el.dataset, TEST_DATASET);
- root.appendChild(el);
- vm = initIde(el, { extendStore });
- };
-
- it('runs', () => {
- createComponent();
-
- expect(root).toMatchSnapshot();
});
it('user commits changes', async () => {
- createComponent();
+ vm = startWebIDE(container);
await ideHelper.createFile('foo/bar/test.txt', 'Lorem ipsum dolar sit');
await ideHelper.deleteFile('foo/bar/.gitkeep');
@@ -89,7 +56,7 @@ describe('WebIDE', () => {
});
it('user adds file that starts with +', async () => {
- createComponent();
+ vm = startWebIDE(container);
await ideHelper.createFile('+test', 'Hello world!');
await ideHelper.openFile('+test');
@@ -101,4 +68,93 @@ describe('WebIDE', () => {
const tabs = Array.from(document.querySelectorAll('.multi-file-tab'));
expect(tabs.map(x => x.textContent.trim())).toEqual(['+test']);
});
+
+ describe('editor info', () => {
+ let statusBar;
+ let editor;
+
+ const waitForEditor = async () => {
+ editor = await ideHelper.waitForMonacoEditor();
+ };
+
+ const changeEditorPosition = async (lineNumber, column) => {
+ editor.setPosition({ lineNumber, column });
+
+ await vm.$nextTick();
+ };
+
+ beforeEach(async () => {
+ vm = startWebIDE(container);
+
+ await ideHelper.openFile('README.md');
+ editor = await ideHelper.waitForMonacoEditor();
+
+ statusBar = ideHelper.getStatusBar();
+ });
+
+ it('shows line position and type', () => {
+ expect(statusBar).toHaveText('1:1');
+ expect(statusBar).toHaveText('markdown');
+ });
+
+ it('persists viewer', async () => {
+ const markdownPreview = 'test preview_markdown result';
+ mockServer.post('/:namespace/:project/preview_markdown', () => ({
+ body: markdownPreview,
+ }));
+
+ await ideHelper.openFile('README.md');
+ ideHelper.clickPreviewMarkdown();
+
+ const el = await waitForText(markdownPreview);
+ expect(el).toHaveText(markdownPreview);
+
+ // Need to wait for monaco editor to load so it doesn't through errors on dispose
+ await ideHelper.openFile('.gitignore');
+ await ideHelper.waitForMonacoEditor();
+ await ideHelper.openFile('README.md');
+ await ideHelper.waitForMonacoEditor();
+
+ expect(el).toHaveText(markdownPreview);
+ });
+
+ describe('when editor position changes', () => {
+ beforeEach(async () => {
+ await changeEditorPosition(4, 10);
+ });
+
+ it('shows new line position', () => {
+ expect(statusBar).not.toHaveText('1:1');
+ expect(statusBar).toHaveText('4:10');
+ });
+
+ it('updates after rename', async () => {
+ await ideHelper.renameFile('README.md', 'READMEZ.txt');
+ await waitForEditor();
+
+ expect(statusBar).toHaveText('1:1');
+ expect(statusBar).toHaveText('plaintext');
+ });
+
+ it('persists position after opening then rename', async () => {
+ await ideHelper.openFile('files/js/application.js');
+ await waitForEditor();
+ await ideHelper.renameFile('README.md', 'READING_RAINBOW.md');
+ await ideHelper.openFile('READING_RAINBOW.md');
+ await waitForEditor();
+
+ expect(statusBar).toHaveText('4:10');
+ expect(statusBar).toHaveText('markdown');
+ });
+
+ it('persists position after closing', async () => {
+ await ideHelper.closeFile('README.md');
+ await ideHelper.openFile('README.md');
+ await waitForEditor();
+
+ expect(statusBar).toHaveText('4:10');
+ expect(statusBar).toHaveText('markdown');
+ });
+ });
+ });
});
diff --git a/spec/frontend_integration/ide/user_opens_file_spec.js b/spec/frontend_integration/ide/user_opens_file_spec.js
new file mode 100644
index 00000000000..98a73c7a029
--- /dev/null
+++ b/spec/frontend_integration/ide/user_opens_file_spec.js
@@ -0,0 +1,89 @@
+import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
+import { screen } from '@testing-library/dom';
+import * as ideHelper from './helpers/ide_helper';
+import startWebIDE from './helpers/start';
+
+// https://gitlab.com/gitlab-org/gitlab/-/issues/293654#note_466432769
+// eslint-disable-next-line jest/no-disabled-tests
+describe.skip('IDE: User opens a file in the Web IDE', () => {
+ useOverclockTimers();
+
+ let vm;
+ let container;
+
+ beforeEach(async () => {
+ setFixtures('<div class="webide-container"></div>');
+ container = document.querySelector('.webide-container');
+
+ vm = startWebIDE(container);
+
+ await screen.findByText('README'); // wait for file tree to load
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ vm = null;
+ });
+
+ describe('user opens a directory', () => {
+ beforeEach(async () => {
+ await ideHelper.openFile('files/images');
+ await screen.findByText('logo-white.png');
+ });
+
+ it('expands directory in the left sidebar', () => {
+ expect(ideHelper.getFilesList()).toEqual(
+ expect.arrayContaining(['html', 'js', 'images', 'logo-white.png']),
+ );
+ });
+ });
+
+ describe('user opens a text file', () => {
+ beforeEach(async () => {
+ await ideHelper.openFile('README.md');
+ await ideHelper.waitForTabToOpen('README.md');
+ });
+
+ it('opens the file in monaco editor', async () => {
+ expect(await ideHelper.getEditorValue()).toContain('Sample repo for testing gitlab features');
+ });
+
+ describe('user switches to review mode', () => {
+ beforeEach(() => {
+ ideHelper.switchLeftSidebarTab('Review');
+ });
+
+ it('shows diff editor', async () => {
+ expect(await ideHelper.findMonacoDiffEditor()).toBeDefined();
+ });
+ });
+ });
+
+ describe('user opens an image file', () => {
+ beforeEach(async () => {
+ await ideHelper.openFile('files/images/logo-white.png');
+ await ideHelper.waitForTabToOpen('logo-white.png');
+ });
+
+ it('opens image viewer for the file', async () => {
+ const viewer = await screen.findByTestId('image-viewer');
+ const img = viewer.querySelector('img');
+
+ expect(img.src).toContain('logo-white.png');
+ });
+ });
+
+ describe('user opens a binary file', () => {
+ beforeEach(async () => {
+ await ideHelper.openFile('Gemfile.zip');
+ await ideHelper.waitForTabToOpen('Gemfile.zip');
+ });
+
+ it('opens image viewer for the file', async () => {
+ const downloadButton = await screen.findByText('Download');
+
+ expect(downloadButton.getAttribute('download')).toEqual('Gemfile.zip');
+ expect(downloadButton.getAttribute('href')).toContain('/raw/');
+ });
+ });
+});
diff --git a/spec/frontend_integration/ide/user_opens_ide_spec.js b/spec/frontend_integration/ide/user_opens_ide_spec.js
new file mode 100644
index 00000000000..502cb2e2c7d
--- /dev/null
+++ b/spec/frontend_integration/ide/user_opens_ide_spec.js
@@ -0,0 +1,160 @@
+import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
+import { screen } from '@testing-library/dom';
+import * as ideHelper from './helpers/ide_helper';
+import startWebIDE from './helpers/start';
+
+describe('IDE: User opens IDE', () => {
+ useOverclockTimers();
+
+ let vm;
+ let container;
+
+ beforeEach(() => {
+ setFixtures('<div class="webide-container"></div>');
+ container = document.querySelector('.webide-container');
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ vm = null;
+ });
+
+ it('shows loading indicator while the IDE is loading', async () => {
+ vm = startWebIDE(container);
+
+ expect(container.querySelectorAll('.multi-file-loading-container')).toHaveLength(3);
+ });
+
+ describe('when the project is empty', () => {
+ beforeEach(() => {
+ vm = startWebIDE(container, { isRepoEmpty: true });
+ });
+
+ it('shows "No files" in the left sidebar', async () => {
+ expect(await screen.findByText('No files')).toBeDefined();
+ });
+
+ it('shows a "New file" button', async () => {
+ const button = await screen.findByTitle('New file');
+
+ expect(button.tagName).toEqual('BUTTON');
+ });
+ });
+
+ describe('when the file tree is loaded', () => {
+ beforeEach(async () => {
+ vm = startWebIDE(container);
+
+ await screen.findByText('README'); // wait for file tree to load
+ });
+
+ it('shows a list of files in the left sidebar', async () => {
+ expect(ideHelper.getFilesList()).toEqual(
+ expect.arrayContaining(['README', 'LICENSE', 'CONTRIBUTING.md']),
+ );
+ });
+
+ it('shows empty state in the main editor window', async () => {
+ expect(
+ await screen.findByText(
+ "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes.",
+ ),
+ ).toBeDefined();
+ });
+
+ it('shows commit button in disabled state', async () => {
+ const button = await screen.findByTestId('begin-commit-button');
+
+ expect(button.getAttribute('disabled')).toBeDefined();
+ });
+
+ it('shows branch/MR dropdown with master selected', async () => {
+ const dropdown = await screen.findByTestId('ide-nav-dropdown');
+
+ expect(dropdown.textContent).toContain('master');
+ });
+ });
+
+ describe('a path to a text file is present in the URL', () => {
+ beforeEach(async () => {
+ vm = startWebIDE(container, { path: 'README.md' });
+
+ await ideHelper.waitForTabToOpen('README.md');
+ });
+
+ it('opens the file and its contents are shown in Monaco', async () => {
+ expect(await ideHelper.getEditorValue()).toContain('Sample repo for testing gitlab features');
+ });
+ });
+
+ describe('a path to a binary file is present in the URL', () => {
+ beforeEach(async () => {
+ vm = startWebIDE(container, { path: 'Gemfile.zip' });
+
+ await ideHelper.waitForTabToOpen('Gemfile.zip');
+ });
+
+ it('shows download viewer', async () => {
+ const downloadButton = await screen.findByText('Download');
+
+ expect(downloadButton.getAttribute('download')).toEqual('Gemfile.zip');
+ expect(downloadButton.getAttribute('href')).toContain('/raw/');
+ });
+ });
+
+ describe('a path to an image is present in the URL', () => {
+ beforeEach(async () => {
+ vm = startWebIDE(container, { path: 'files/images/logo-white.png' });
+
+ await ideHelper.waitForTabToOpen('logo-white.png');
+ });
+
+ it('shows image viewer', async () => {
+ const viewer = await screen.findByTestId('image-viewer');
+ const img = viewer.querySelector('img');
+
+ expect(img.src).toContain('logo-white.png');
+ });
+ });
+
+ describe('path in URL is a directory', () => {
+ beforeEach(async () => {
+ vm = startWebIDE(container, { path: 'files/images' });
+
+ // wait for folders in left sidebar to be expanded
+ await screen.findByText('images');
+ });
+
+ it('expands folders in the left sidebar', () => {
+ expect(ideHelper.getFilesList()).toEqual(
+ expect.arrayContaining(['files', 'images', 'logo-white.png', 'logo-black.png']),
+ );
+ });
+
+ it('shows empty state in the main editor window', async () => {
+ expect(
+ await screen.findByText(
+ "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes.",
+ ),
+ ).toBeDefined();
+ });
+ });
+
+ describe("a file for path in url doesn't exist in the repo", () => {
+ beforeEach(async () => {
+ vm = startWebIDE(container, { path: 'abracadabra/hocus-focus.txt' });
+
+ await ideHelper.waitForTabToOpen('hocus-focus.txt');
+ });
+
+ it('create new folders and file in the left sidebar', () => {
+ expect(ideHelper.getFilesList()).toEqual(
+ expect.arrayContaining(['abracadabra', 'hocus-focus.txt']),
+ );
+ });
+
+ it('creates a blank new file', async () => {
+ expect(await ideHelper.getEditorValue()).toEqual('\n');
+ });
+ });
+});