Commit 6a01be49 authored by Himanshu Kapoor's avatar Himanshu Kapoor Committed by Kushal Pandya

Add FE integration tests for "User opens Web IDE"

parent 3b46ce3c
...@@ -45,7 +45,7 @@ export default { ...@@ -45,7 +45,7 @@ export default {
</script> </script>
<template> <template>
<div ref="dropdown" class="btn-group ide-nav-dropdown dropdown"> <div ref="dropdown" class="btn-group ide-nav-dropdown dropdown" data-testid="ide-nav-dropdown">
<nav-dropdown-button :show-merge-requests="canReadMergeRequests" /> <nav-dropdown-button :show-merge-requests="canReadMergeRequests" />
<div class="dropdown-menu dropdown-menu-left p-0"> <div class="dropdown-menu dropdown-menu-left p-0">
<nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" /> <nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" />
......
...@@ -77,7 +77,7 @@ export default { ...@@ -77,7 +77,7 @@ export default {
</script> </script>
<template> <template>
<div> <div data-testid="image-viewer">
<div :class="innerCssClasses" class="position-relative"> <div :class="innerCssClasses" class="position-relative">
<img ref="contentImg" :src="path" @load="onImgLoad" /> <slot name="image-overlay"></slot> <img ref="contentImg" :src="path" @load="onImgLoad" /> <slot name="image-overlay"></slot>
</div> </div>
......
...@@ -143,6 +143,7 @@ export default { ...@@ -143,6 +143,7 @@ export default {
:style="levelIndentation" :style="levelIndentation"
class="file-row-name" class="file-row-name"
data-qa-selector="file_name_content" data-qa-selector="file_name_content"
data-testid="file-row-name-container"
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]" :class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
> >
<file-icon <file-icon
......
...@@ -13,6 +13,9 @@ RSpec.describe 'Raw files', '(JavaScript fixtures)' do ...@@ -13,6 +13,9 @@ RSpec.describe 'Raw files', '(JavaScript fixtures)' do
clean_frontend_fixtures('blob/balsamiq/') clean_frontend_fixtures('blob/balsamiq/')
clean_frontend_fixtures('blob/notebook/') clean_frontend_fixtures('blob/notebook/')
clean_frontend_fixtures('blob/pdf/') clean_frontend_fixtures('blob/pdf/')
clean_frontend_fixtures('blob/text/')
clean_frontend_fixtures('blob/binary/')
clean_frontend_fixtures('blob/images/')
end end
after do after do
...@@ -38,4 +41,16 @@ RSpec.describe 'Raw files', '(JavaScript fixtures)' do ...@@ -38,4 +41,16 @@ RSpec.describe 'Raw files', '(JavaScript fixtures)' do
it 'blob/pdf/test.pdf' do it 'blob/pdf/test.pdf' do
@blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf') @blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf')
end end
it 'blob/text/README.md' do
@blob = project.repository.blob_at('e774ebd33', 'README.md')
end
it 'blob/images/logo-white.png' do
@blob = project.repository.blob_at('e774ebd33', 'files/images/logo-white.png')
end
it 'blob/binary/Gemfile.zip' do
@blob = project.repository.blob_at('e774ebd33', 'Gemfile.zip')
end
end end
// 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>
`;
import { TEST_HOST } from 'helpers/test_constants';
import { findAllByText, fireEvent, getByLabelText, screen } from '@testing-library/dom'; import { findAllByText, fireEvent, getByLabelText, screen } from '@testing-library/dom';
import { initIde } from '~/ide';
import extendStore from '~/ide/stores/extend';
import { IDE_DATASET } from './mock_data';
const isFolderRowOpen = row => row.matches('.folder.is-open'); const isFolderRowOpen = row => row.matches('.folder.is-open');
...@@ -12,16 +16,23 @@ const clickOnLeftSidebarTab = name => { ...@@ -12,16 +16,23 @@ const clickOnLeftSidebarTab = name => {
button.click(); button.click();
}; };
const findMonacoEditor = () => export const findMonacoEditor = () =>
screen.findByLabelText(/Editor content;/).then(x => x.closest('.monaco-editor')); screen.findByLabelText(/Editor content;/).then(x => x.closest('.monaco-editor'));
const findAndSetEditorValue = async value => { export const findAndSetEditorValue = async value => {
const editor = await findMonacoEditor(); const editor = await findMonacoEditor();
const uri = editor.getAttribute('data-uri'); const uri = editor.getAttribute('data-uri');
window.monaco.editor.getModel(uri).setValue(value); 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 findTreeBody = () => screen.findByTestId('ide-tree-body', {}, { timeout: 5000 });
const findRootActions = () => screen.findByTestId('ide-root-actions', {}, { timeout: 7000 }); const findRootActions = () => screen.findByTestId('ide-root-actions', {}, { timeout: 7000 });
...@@ -107,6 +118,10 @@ export const createFile = async (path, content) => { ...@@ -107,6 +118,10 @@ export const createFile = async (path, content) => {
await findAndSetEditorValue(content); await findAndSetEditorValue(content);
}; };
export const getFilesList = () => {
return screen.getAllByTestId('file-row-name-container').map(e => e.textContent.trim());
};
export const deleteFile = async path => { export const deleteFile = async path => {
const row = await findAndTraverseToPath(path); const row = await findAndTraverseToPath(path);
clickFileRowAction(row, 'Delete'); clickFileRowAction(row, 'Delete');
...@@ -120,3 +135,16 @@ export const commit = async () => { ...@@ -120,3 +135,16 @@ export const commit = async () => {
screen.getByText('Commit').click(); screen.getByText('Commit').click();
}; };
export const createIdeComponent = (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 });
};
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',
};
import { TEST_HOST } from 'helpers/test_constants';
import { waitForText } from 'helpers/wait_for_text'; import { waitForText } from 'helpers/wait_for_text';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { useOverclockTimers } from 'test_helpers/utils/overclock_timers'; import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import { createCommitId } from 'test_helpers/factories/commit_id'; import { createCommitId } from 'test_helpers/factories/commit_id';
import { initIde } from '~/ide'; import * as ideHelper from './helpers/ide_helper';
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',
};
describe('WebIDE', () => { describe('WebIDE', () => {
useOverclockTimers(); useOverclockTimers();
let vm; let vm;
let root; let container;
beforeEach(() => { beforeEach(() => {
root = document.createElement('div'); setFixtures('<div class="webide-container"></div>');
document.body.appendChild(root); container = document.querySelector('.webide-container');
global.jsdom.reconfigure({
url: `${TEST_HOST}/-/ide/project/gitlab-test/lorem-ipsum`,
});
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
vm = null; 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 () => { it('user commits changes', async () => {
createComponent(); vm = ideHelper.createIdeComponent(container);
await ideHelper.createFile('foo/bar/test.txt', 'Lorem ipsum dolar sit'); await ideHelper.createFile('foo/bar/test.txt', 'Lorem ipsum dolar sit');
await ideHelper.deleteFile('foo/bar/.gitkeep'); await ideHelper.deleteFile('foo/bar/.gitkeep');
...@@ -89,7 +55,7 @@ describe('WebIDE', () => { ...@@ -89,7 +55,7 @@ describe('WebIDE', () => {
}); });
it('user adds file that starts with +', async () => { it('user adds file that starts with +', async () => {
createComponent(); vm = ideHelper.createIdeComponent(container);
await ideHelper.createFile('+test', 'Hello world!'); await ideHelper.createFile('+test', 'Hello world!');
await ideHelper.openFile('+test'); await ideHelper.openFile('+test');
......
import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import { findByText, screen } from '@testing-library/dom';
import * as ideHelper from './helpers/ide_helper';
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 = ideHelper.createIdeComponent(container);
expect(container.querySelectorAll('.multi-file-loading-container')).toHaveLength(3);
});
describe('when the project is empty', () => {
beforeEach(() => {
vm = ideHelper.createIdeComponent(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 = ideHelper.createIdeComponent(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 = ideHelper.createIdeComponent(container, { path: 'README.md' });
// a new tab is open for README.md
await findByText(document.querySelector('.multi-file-edit-pane'), '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 = ideHelper.createIdeComponent(container, { path: 'Gemfile.zip' });
// a new tab is open for Gemfile.zip
await findByText(document.querySelector('.multi-file-edit-pane'), '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 = ideHelper.createIdeComponent(container, { path: 'files/images/logo-white.png' });
// a new tab is open for logo-white.png
await findByText(document.querySelector('.multi-file-edit-pane'), '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 = ideHelper.createIdeComponent(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 = ideHelper.createIdeComponent(container, { path: 'abracadabra/hocus-focus.txt' });
// a new tab is open for hocus-focus.txt
await findByText(document.querySelector('.multi-file-edit-pane'), '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');
});
});
});
/* eslint-disable global-require, import/no-unresolved */ /* eslint-disable global-require, import/no-unresolved */
import { memoize } from 'lodash'; import { memoize } from 'lodash';
import { readFileSync } from 'fs';
import { join } from 'path';
export const getProject = () => require('test_fixtures/api/projects/get.json'); export const getProject = () => require('test_fixtures/api/projects/get.json');
export const getEmptyProject = () => require('test_fixtures/api/projects/get_empty.json');
export const getBranch = () => require('test_fixtures/api/projects/branches/get.json'); export const getBranch = () => require('test_fixtures/api/projects/branches/get.json');
export const getMergeRequests = () => require('test_fixtures/api/merge_requests/get.json'); export const getMergeRequests = () => require('test_fixtures/api/merge_requests/get.json');
export const getRepositoryFiles = () => require('test_fixtures/projects_json/files.json'); export const getRepositoryFiles = () => require('test_fixtures/projects_json/files.json');
export const getBlobReadme = () =>
readFileSync(require.resolve('test_fixtures/blob/text/README.md'), 'utf8');
export const getBlobZip = () =>
readFileSync(require.resolve('test_fixtures/blob/binary/Gemfile.zip'), 'utf8');
export const getBlobImage = () =>
readFileSync(
join(require.resolve('test_fixtures/blob/text/README.md'), '../..', 'images/logo-white.png'),
'utf8',
);
export const getPipelinesEmptyResponse = () => export const getPipelinesEmptyResponse = () =>
require('test_fixtures/projects_json/pipelines_empty.json'); require('test_fixtures/projects_json/pipelines_empty.json');
export const getCommit = memoize(() => getBranch().commit); export const getCommit = memoize(() => getBranch().commit);
import { Server, Model, RestSerializer } from 'miragejs'; import { Server, Model, RestSerializer } from 'miragejs';
import { getProject, getBranch, getMergeRequests, getRepositoryFiles } from 'test_helpers/fixtures'; import {
getProject,
getEmptyProject,
getBranch,
getMergeRequests,
getRepositoryFiles,
getBlobReadme,
getBlobImage,
getBlobZip,
} from 'test_helpers/fixtures';
import setupRoutes from './routes'; import setupRoutes from './routes';
export const createMockServerOptions = () => ({ export const createMockServerOptions = () => ({
...@@ -18,9 +27,23 @@ export const createMockServerOptions = () => ({ ...@@ -18,9 +27,23 @@ export const createMockServerOptions = () => ({
seeds(schema) { seeds(schema) {
schema.db.loadData({ schema.db.loadData({
files: getRepositoryFiles().map(path => ({ path })), files: getRepositoryFiles().map(path => ({ path })),
projects: [getProject()], projects: [getProject(), getEmptyProject()],
branches: [getBranch()], branches: [getBranch()],
mergeRequests: getMergeRequests(), mergeRequests: getMergeRequests(),
filesRaw: [
{
raw: getBlobReadme(),
path: 'README.md',
},
{
raw: getBlobZip(),
path: 'Gemfile.zip',
},
{
raw: getBlobImage(),
path: 'files/images/logo-white.png',
},
],
userPermissions: [ userPermissions: [
{ {
createMergeRequestIn: true, createMergeRequestIn: true,
......
...@@ -19,6 +19,18 @@ export default server => { ...@@ -19,6 +19,18 @@ export default server => {
return schema.db.files.map(({ path }) => path); return schema.db.files.map(({ path }) => path);
}); });
server.get('/:namespace/:project/-/blob/:sha/*path', (schema, request) => {
const { path } = schema.db.files.findBy({ path: request.params.path });
return { path, rawPath: request.url.replace('/-/blob', '/-/raw') };
});
server.get('/:namespace/:project/-/raw/:sha/*path', (schema, request) => {
const { path } = request.params;
return schema.db.filesRaw.findBy({ path })?.raw || 'Sample content';
});
server.post('/api/v4/projects/:id/repository/commits', (schema, request) => { server.post('/api/v4/projects/:id/repository/commits', (schema, request) => {
const { branch: branchName, commit_message: message, actions } = JSON.parse( const { branch: branchName, commit_message: message, actions } = JSON.parse(
request.requestBody, request.requestBody,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment