Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b852029507
commit
38de2aa494
29 changed files with 465 additions and 176 deletions
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import {
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
|
@ -37,7 +38,7 @@ export default {
|
|||
return {
|
||||
search: '',
|
||||
participants: [],
|
||||
selected: this.$store.getters.activeIssue.assignees,
|
||||
selected: [],
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
@ -89,9 +90,20 @@ export default {
|
|||
isSearchEmpty() {
|
||||
return this.search === '';
|
||||
},
|
||||
currentUser() {
|
||||
return gon?.current_username;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.selected = cloneDeep(this.activeIssue.assignees);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setAssignees']),
|
||||
async assignSelf() {
|
||||
const [currentUserObject] = await this.setAssignees(this.currentUser);
|
||||
|
||||
this.selectAssignee(currentUserObject);
|
||||
},
|
||||
clearSelected() {
|
||||
this.selected = [];
|
||||
},
|
||||
|
@ -119,7 +131,7 @@ export default {
|
|||
<template>
|
||||
<board-editable-item :title="assigneeText" @close="saveAssignees">
|
||||
<template #collapsed>
|
||||
<issuable-assignees :users="activeIssue.assignees" />
|
||||
<issuable-assignees :users="selected" @assign-self="assignSelf" />
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
|
|
|
@ -325,11 +325,15 @@ export default {
|
|||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const { nodes } = data.issueSetAssignees?.issue?.assignees || [];
|
||||
|
||||
commit('UPDATE_ISSUE_BY_ID', {
|
||||
issueId: getters.activeIssue.id,
|
||||
prop: 'assignees',
|
||||
value: data.issueSetAssignees.issue.assignees.nodes,
|
||||
value: nodes,
|
||||
});
|
||||
|
||||
return nodes;
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<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" />
|
||||
<div class="dropdown-menu dropdown-menu-left p-0">
|
||||
<nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" />
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { n__ } from '~/locale';
|
||||
import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
UncollapsedAssigneeList,
|
||||
},
|
||||
inject: ['rootPath'],
|
||||
|
@ -27,9 +29,15 @@ export default {
|
|||
<template>
|
||||
<div class="gl-display-flex gl-flex-direction-column">
|
||||
<div v-if="emptyUsers" data-testid="none">
|
||||
<span>
|
||||
{{ __('None') }}
|
||||
</span>
|
||||
<span> {{ __('None') }} -</span>
|
||||
<gl-button
|
||||
data-testid="assign-yourself"
|
||||
category="tertiary"
|
||||
variant="link"
|
||||
@click="$emit('assign-self')"
|
||||
>
|
||||
<span class="gl-text-gray-400">{{ __('assign yourself') }}</span>
|
||||
</gl-button>
|
||||
</div>
|
||||
<uncollapsed-assignee-list v-else :users="users" :root-path="rootPath" />
|
||||
</div>
|
||||
|
|
|
@ -77,7 +77,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div data-testid="image-viewer">
|
||||
<div :class="innerCssClasses" class="position-relative">
|
||||
<img ref="contentImg" :src="path" @load="onImgLoad" /> <slot name="image-overlay"></slot>
|
||||
</div>
|
||||
|
|
|
@ -143,6 +143,7 @@ export default {
|
|||
:style="levelIndentation"
|
||||
class="file-row-name"
|
||||
data-qa-selector="file_name_content"
|
||||
data-testid="file-row-name-container"
|
||||
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
|
||||
>
|
||||
<file-icon
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
<script>
|
||||
import Pikaday from 'pikaday';
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'DatePicker',
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
|
@ -66,7 +70,7 @@ export default {
|
|||
<div class="dropdown open">
|
||||
<button type="button" class="dropdown-menu-toggle" data-toggle="dropdown" @click="toggled">
|
||||
<span class="dropdown-toggle-text"> {{ label }} </span>
|
||||
<i class="fa fa-chevron-down" aria-hidden="true"> </i>
|
||||
<gl-icon name="chevron-down" class="gl-absolute gl-right-3 gl-top-3 gl-text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,11 +14,15 @@ module Ci
|
|||
end
|
||||
|
||||
def set_data(model, new_data)
|
||||
# TODO: Support AWS S3 server side encryption
|
||||
files.create({
|
||||
key: key(model),
|
||||
body: new_data
|
||||
})
|
||||
if Feature.enabled?(:ci_live_trace_use_fog_attributes)
|
||||
files.create(create_attributes(model, new_data))
|
||||
else
|
||||
# TODO: Support AWS S3 server side encryption
|
||||
files.create({
|
||||
key: key(model),
|
||||
body: new_data
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def append_data(model, new_data, offset)
|
||||
|
@ -57,6 +61,13 @@ module Ci
|
|||
key_raw(model.build_id, model.chunk_index)
|
||||
end
|
||||
|
||||
def create_attributes(model, new_data)
|
||||
{
|
||||
key: key(model),
|
||||
body: new_data
|
||||
}.merge(object_store_config.fog_attributes)
|
||||
end
|
||||
|
||||
def key_raw(build_id, chunk_index)
|
||||
"tmp/builds/#{build_id.to_i}/chunks/#{chunk_index.to_i}.log"
|
||||
end
|
||||
|
@ -84,6 +95,14 @@ module Ci
|
|||
def object_store
|
||||
Gitlab.config.artifacts.object_store
|
||||
end
|
||||
|
||||
def object_store_raw_config
|
||||
object_store
|
||||
end
|
||||
|
||||
def object_store_config
|
||||
@object_store_config ||= ::ObjectStorage::Config.new(object_store_raw_config)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.dropdown.inline.gl-ml-3
|
||||
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
|
||||
= sorted_by
|
||||
= icon('chevron-down')
|
||||
= sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
|
||||
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
|
||||
%li
|
||||
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sorted_by)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate chevron-down icon to svg
|
||||
merge_request: 47591
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace fa-chevron-down icon in pikaday
|
||||
merge_request: 48054
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Support S3 server side encryption in CI cloud native job logs
|
||||
merge_request: 47536
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/ss-assign-self.yml
Normal file
5
changelogs/unreleased/ss-assign-self.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add assign self to group boards sidebar
|
||||
merge_request: 47705
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_live_trace_use_fog_attributes
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47536
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285079
|
||||
milestone: '13.6'
|
||||
type: development
|
||||
group: group::testing
|
||||
default_enabled: false
|
|
@ -228,6 +228,7 @@ with the following properties:
|
|||
| ---------------------- | -------------------------------------------------------------------------------------- |
|
||||
| `description` | A description of the code quality violation. |
|
||||
| `fingerprint` | A unique fingerprint to identify the code quality violation. For example, an MD5 hash. |
|
||||
| `severity` | A severity string (can be `info`, `minor`, `major`, `critical`, or `blocker`). |
|
||||
| `location.path` | The relative path to the file containing the code quality violation. |
|
||||
| `location.lines.begin` | The line on which the code quality violation occurred. |
|
||||
|
||||
|
@ -238,6 +239,7 @@ Example:
|
|||
{
|
||||
"description": "'unused' is assigned a value but never used.",
|
||||
"fingerprint": "7815696ecbf1c96e6894b779456d330e",
|
||||
"severity": "minor",
|
||||
"location": {
|
||||
"path": "lib/index.js",
|
||||
"lines": {
|
||||
|
|
|
@ -93,6 +93,11 @@ module ObjectStorage
|
|||
|
||||
private
|
||||
|
||||
# This returns a Hash of HTTP encryption headers to send along to S3.
|
||||
#
|
||||
# They can also be passed in as Fog::AWS::Storage::File attributes, since there
|
||||
# are aliases defined for them:
|
||||
# https://github.com/fog/fog-aws/blob/ab288f29a0974d64fd8290db41080e5578be9651/lib/fog/aws/models/storage/file.rb#L24-L25
|
||||
def aws_server_side_encryption_headers
|
||||
{
|
||||
'x-amz-server-side-encryption' => server_side_encryption,
|
||||
|
|
|
@ -20,6 +20,7 @@ describe('BoardCardAssigneeDropdown', () => {
|
|||
let fakeApollo;
|
||||
let getIssueParticipantsSpy;
|
||||
let getSearchUsersSpy;
|
||||
let dispatchSpy;
|
||||
|
||||
const iid = '111';
|
||||
const activeIssueName = 'test';
|
||||
|
@ -91,10 +92,11 @@ describe('BoardCardAssigneeDropdown', () => {
|
|||
},
|
||||
};
|
||||
|
||||
jest.spyOn(store, 'dispatch').mockResolvedValue();
|
||||
dispatchSpy = jest.spyOn(store, 'dispatch').mockResolvedValue();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.gon = {};
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
@ -305,4 +307,25 @@ describe('BoardCardAssigneeDropdown', () => {
|
|||
|
||||
expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when assign-self is emitted from IssuableAssignees', () => {
|
||||
const currentUser = { username: 'self', name: '', id: '' };
|
||||
|
||||
beforeEach(() => {
|
||||
window.gon = { current_username: currentUser.username };
|
||||
|
||||
dispatchSpy.mockResolvedValue([currentUser]);
|
||||
createComponent();
|
||||
|
||||
wrapper.find(IssuableAssignees).vm.$emit('assign-self');
|
||||
});
|
||||
|
||||
it('calls setAssignees with currentUser', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith('setAssignees', currentUser.username);
|
||||
});
|
||||
|
||||
it('adds the user to the selected list', async () => {
|
||||
expect(findByText(currentUser.username).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,9 @@ RSpec.describe 'Raw files', '(JavaScript fixtures)' do
|
|||
clean_frontend_fixtures('blob/balsamiq/')
|
||||
clean_frontend_fixtures('blob/notebook/')
|
||||
clean_frontend_fixtures('blob/pdf/')
|
||||
clean_frontend_fixtures('blob/text/')
|
||||
clean_frontend_fixtures('blob/binary/')
|
||||
clean_frontend_fixtures('blob/images/')
|
||||
end
|
||||
|
||||
after do
|
||||
|
@ -38,4 +41,16 @@ RSpec.describe 'Raw files', '(JavaScript fixtures)' do
|
|||
it 'blob/pdf/test.pdf' do
|
||||
@blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf')
|
||||
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
|
||||
|
|
|
@ -26,8 +26,8 @@ describe('IssuableAssignees', () => {
|
|||
createComponent();
|
||||
});
|
||||
|
||||
it('renders "None"', () => {
|
||||
expect(findEmptyAssignee().text()).toBe('None');
|
||||
it('renders "None - assign yourself"', () => {
|
||||
expect(findEmptyAssignee().text()).toBe('None - assign yourself');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -38,4 +38,12 @@ describe('IssuableAssignees', () => {
|
|||
expect(findUncollapsedAssigneeList().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when clicking "assign yourself"', () => {
|
||||
it('emits "assign-self"', () => {
|
||||
createComponent();
|
||||
wrapper.find('[data-testid="assign-yourself"]').vm.$emit('click');
|
||||
expect(wrapper.emitted('assign-self')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -1,4 +1,8 @@
|
|||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
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');
|
||||
|
||||
|
@ -12,16 +16,23 @@ const clickOnLeftSidebarTab = name => {
|
|||
button.click();
|
||||
};
|
||||
|
||||
const findMonacoEditor = () =>
|
||||
export const findMonacoEditor = () =>
|
||||
screen.findByLabelText(/Editor content;/).then(x => x.closest('.monaco-editor'));
|
||||
|
||||
const findAndSetEditorValue = async value => {
|
||||
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 });
|
||||
|
@ -107,6 +118,10 @@ export const createFile = async (path, content) => {
|
|||
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');
|
||||
|
@ -120,3 +135,16 @@ export const commit = async () => {
|
|||
|
||||
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 });
|
||||
};
|
12
spec/frontend_integration/ide/helpers/mock_data.js
Normal file
12
spec/frontend_integration/ide/helpers/mock_data.js
Normal file
|
@ -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',
|
||||
};
|
|
@ -1,61 +1,27 @@
|
|||
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';
|
||||
|
||||
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 = ideHelper.createIdeComponent(container);
|
||||
|
||||
await ideHelper.createFile('foo/bar/test.txt', 'Lorem ipsum dolar sit');
|
||||
await ideHelper.deleteFile('foo/bar/.gitkeep');
|
||||
|
@ -89,7 +55,7 @@ describe('WebIDE', () => {
|
|||
});
|
||||
|
||||
it('user adds file that starts with +', async () => {
|
||||
createComponent();
|
||||
vm = ideHelper.createIdeComponent(container);
|
||||
|
||||
await ideHelper.createFile('+test', 'Hello world!');
|
||||
await ideHelper.openFile('+test');
|
||||
|
|
163
spec/frontend_integration/ide/user_opens_ide_spec.js
Normal file
163
spec/frontend_integration/ide/user_opens_ide_spec.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,10 +1,24 @@
|
|||
/* eslint-disable global-require, import/no-unresolved */
|
||||
import { memoize } from 'lodash';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
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 getMergeRequests = () => require('test_fixtures/api/merge_requests/get.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 = () =>
|
||||
require('test_fixtures/projects_json/pipelines_empty.json');
|
||||
export const getCommit = memoize(() => getBranch().commit);
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
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';
|
||||
|
||||
export const createMockServerOptions = () => ({
|
||||
|
@ -18,9 +27,23 @@ export const createMockServerOptions = () => ({
|
|||
seeds(schema) {
|
||||
schema.db.loadData({
|
||||
files: getRepositoryFiles().map(path => ({ path })),
|
||||
projects: [getProject()],
|
||||
projects: [getProject(), getEmptyProject()],
|
||||
branches: [getBranch()],
|
||||
mergeRequests: getMergeRequests(),
|
||||
filesRaw: [
|
||||
{
|
||||
raw: getBlobReadme(),
|
||||
path: 'README.md',
|
||||
},
|
||||
{
|
||||
raw: getBlobZip(),
|
||||
path: 'Gemfile.zip',
|
||||
},
|
||||
{
|
||||
raw: getBlobImage(),
|
||||
path: 'files/images/logo-white.png',
|
||||
},
|
||||
],
|
||||
userPermissions: [
|
||||
{
|
||||
createMergeRequestIn: true,
|
||||
|
|
|
@ -19,6 +19,18 @@ export default server => {
|
|||
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) => {
|
||||
const { branch: branchName, commit_message: message, actions } = JSON.parse(
|
||||
request.requestBody,
|
||||
|
|
|
@ -74,6 +74,52 @@ RSpec.describe Ci::BuildTraceChunks::Fog do
|
|||
|
||||
expect(data_store.data(model)).to eq new_data
|
||||
end
|
||||
|
||||
context 'when S3 server side encryption is enabled' do
|
||||
before do
|
||||
config = Gitlab.config.artifacts.object_store.to_h
|
||||
config[:storage_options] = { server_side_encryption: 'AES256' }
|
||||
allow(data_store).to receive(:object_store_raw_config).and_return(config)
|
||||
end
|
||||
|
||||
it 'creates a file with attributes' do
|
||||
expect_next_instance_of(Fog::AWS::Storage::Files) do |files|
|
||||
expect(files).to receive(:create).with(
|
||||
hash_including(
|
||||
key: anything,
|
||||
body: new_data,
|
||||
'x-amz-server-side-encryption' => 'AES256')
|
||||
).and_call_original
|
||||
end
|
||||
|
||||
expect(data_store.data(model)).to be_nil
|
||||
|
||||
data_store.set_data(model, new_data)
|
||||
|
||||
expect(data_store.data(model)).to eq new_data
|
||||
end
|
||||
|
||||
context 'when ci_live_trace_use_fog_attributes flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_live_trace_use_fog_attributes: false)
|
||||
end
|
||||
|
||||
it 'does not pass along Fog attributes' do
|
||||
expect_next_instance_of(Fog::AWS::Storage::Files) do |files|
|
||||
expect(files).to receive(:create).with(
|
||||
key: anything,
|
||||
body: new_data
|
||||
).and_call_original
|
||||
end
|
||||
|
||||
expect(data_store.data(model)).to be_nil
|
||||
|
||||
data_store.set_data(model, new_data)
|
||||
|
||||
expect(data_store.data(model)).to eq new_data
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -22,6 +22,16 @@ module StubObjectStorage
|
|||
background_upload: false,
|
||||
direct_upload: false
|
||||
)
|
||||
new_config = config.to_h.deep_symbolize_keys.merge({
|
||||
enabled: enabled,
|
||||
proxy_download: proxy_download,
|
||||
background_upload: background_upload,
|
||||
direct_upload: direct_upload
|
||||
})
|
||||
|
||||
# Needed for ObjectStorage::Config compatibility
|
||||
allow(config).to receive(:to_hash).and_return(new_config)
|
||||
allow(config).to receive(:to_h).and_return(new_config)
|
||||
allow(config).to receive(:enabled) { enabled }
|
||||
allow(config).to receive(:proxy_download) { proxy_download }
|
||||
allow(config).to receive(:background_upload) { background_upload }
|
||||
|
|
Loading…
Reference in a new issue