提交 68236049 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 2f26f81c
......@@ -134,28 +134,40 @@ export const scrollToTab = () => {
});
};
export const stageAllChanges = ({ state, commit, dispatch }) => {
export const stageAllChanges = ({ state, commit, dispatch, getters }) => {
const openFile = state.openFiles[0];
commit(types.SET_LAST_COMMIT_MSG, '');
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
state.changedFiles.forEach(file =>
commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }),
);
dispatch('openPendingTab', {
file: state.stagedFiles.find(f => f.path === openFile.path),
keyPrefix: stageKeys.staged,
});
const file = getters.getStagedFile(openFile.path);
if (file) {
dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.staged,
});
}
};
export const unstageAllChanges = ({ state, commit, dispatch }) => {
export const unstageAllChanges = ({ state, commit, dispatch, getters }) => {
const openFile = state.openFiles[0];
state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
state.stagedFiles.forEach(file =>
commit(types.UNSTAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }),
);
dispatch('openPendingTab', {
file: state.changedFiles.find(f => f.path === openFile.path),
keyPrefix: stageKeys.unstaged,
});
const file = getters.getChangedFile(openFile.path);
if (file) {
dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.unstaged,
});
}
};
export const updateViewer = ({ commit }, viewer) => {
......
......@@ -214,20 +214,20 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
};
export const stageChange = ({ commit, state, dispatch }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path);
const openFile = state.openFiles.find(f => f.path === path);
export const stageChange = ({ commit, dispatch, getters }, path) => {
const stagedFile = getters.getStagedFile(path);
const openFile = getters.getOpenFile(path);
commit(types.STAGE_CHANGE, path);
commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
commit(types.SET_LAST_COMMIT_MSG, '');
if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
}
if (openFile && openFile.active) {
const file = state.stagedFiles.find(f => f.path === path);
const file = getters.getStagedFile(path);
if (openFile && openFile.active && file) {
dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.staged,
......@@ -235,14 +235,14 @@ export const stageChange = ({ commit, state, dispatch }, path) => {
}
};
export const unstageChange = ({ commit, dispatch, state }, path) => {
const openFile = state.openFiles.find(f => f.path === path);
export const unstageChange = ({ commit, dispatch, getters }, path) => {
const openFile = getters.getOpenFile(path);
commit(types.UNSTAGE_CHANGE, path);
commit(types.UNSTAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
if (openFile && openFile.active) {
const file = state.changedFiles.find(f => f.path === path);
const file = getters.getChangedFile(path);
if (openFile && openFile.active && file) {
dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.unstaged,
......
......@@ -64,6 +64,7 @@ export const allBlobs = state =>
export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
export const getOpenFile = state => path => state.openFiles.find(f => f.path === path);
export const lastOpenedFile = state =>
[...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0];
......
......@@ -164,31 +164,32 @@ export default {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
[types.STAGE_CHANGE](state, path) {
[types.STAGE_CHANGE](state, { path, diffInfo }) {
const stagedFile = state.stagedFiles.find(f => f.path === path);
Object.assign(state, {
changedFiles: state.changedFiles.filter(f => f.path !== path),
entries: Object.assign(state.entries, {
[path]: Object.assign(state.entries[path], {
staged: true,
staged: diffInfo.exists,
changed: diffInfo.changed,
tempFile: diffInfo.tempFile,
deleted: diffInfo.deleted,
}),
}),
});
if (stagedFile) {
Object.assign(stagedFile, {
...state.entries[path],
});
Object.assign(stagedFile, { ...state.entries[path] });
} else {
Object.assign(state, {
stagedFiles: state.stagedFiles.concat({
...state.entries[path],
}),
});
state.stagedFiles = [...state.stagedFiles, { ...state.entries[path] }];
}
if (!diffInfo.exists) {
state.stagedFiles = state.stagedFiles.filter(f => f.path !== path);
}
},
[types.UNSTAGE_CHANGE](state, path) {
[types.UNSTAGE_CHANGE](state, { path, diffInfo }) {
const changedFile = state.changedFiles.find(f => f.path === path);
const stagedFile = state.stagedFiles.find(f => f.path === path);
......@@ -201,9 +202,11 @@ export default {
changed: true,
});
Object.assign(state, {
changedFiles: state.changedFiles.concat(state.entries[path]),
});
state.changedFiles = state.changedFiles.concat(state.entries[path]);
}
if (!diffInfo.exists) {
state.changedFiles = state.changedFiles.filter(f => f.path !== path);
}
Object.assign(state, {
......@@ -211,6 +214,9 @@ export default {
entries: Object.assign(state.entries, {
[path]: Object.assign(state.entries[path], {
staged: false,
changed: diffInfo.changed,
tempFile: diffInfo.tempFile,
deleted: diffInfo.deleted,
}),
}),
});
......
---
title: "!21542 Part 3: Handle edge cases in stage and unstage mutations"
merge_request: 21676
author:
type: fixed
---
title: Turns on backend MR reports for DAST by default
merge_request: 22001
author:
type: changed
......@@ -57,7 +57,7 @@ Parameters:
Creates a two-way relation between two issues. User must be allowed to update both issues in order to succeed.
```
POST /projects/:id/issues/:issue_iid/links/:target_project_id/:target_issue_iid
POST /projects/:id/issues/:issue_iid/links
```
| Attribute | Type | Required | Description |
......@@ -67,6 +67,12 @@ POST /projects/:id/issues/:issue_iid/links/:target_project_id/:target_issue_iid
| `target_project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) of a target project |
| `target_issue_iid` | integer/string | yes | The internal ID of a target project's issue |
```bash
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/issues/1/links?target_project_id=5&target_issue_iid=1"
```
Example response:
```json
{
"source_issue" : {
......
......@@ -22,3 +22,9 @@ guide you through the process. Or check out how other members of the commmunity
are adding support for [PHP](https://gitlab.com/gitlab-org/gitlab/merge_requests/17417) or [Terraform](https://gitlab.com/gitlab-org/gitlab/merge_requests/18834).
NOTE: **Note** We are especially interested in adding support for [PyPi](https://gitlab.com/gitlab-org/gitlab/issues/10483), [RubyGems](https://gitlab.com/gitlab-org/gitlab/issues/803), [Debian](https://gitlab.com/gitlab-org/gitlab/issues/5835), and [RPM](https://gitlab.com/gitlab-org/gitlab/issues/5932).
## Package workflows
Learning how to use the GitLab Package Registry will help you build your own custom package workflow.
[Use a project as a package registry](./workflows/project_registry.md) to publish all of your packages to one project.
# Project as a package registry
Using the features of the package registry, it is possible to use one project to store all of your packages.
This guide mirrors the creation of [this package registry](https://gitlab.com/sabrams/my-package-registry).
For the video version, see [Single Project Package Registry Demo](https://youtu.be/ui2nNBwN35c).
## How does this work?
You might be wondering "how is it possible to upload two packages from different codebases to the same project on GitLab?".
It is easy to forget that a package on GitLab belongs to a project, but a project does not have to be a code repository.
The code used to build your packages can be stored anywhere - maybe it is another project on GitLab, or maybe a completely
different system altogether. All that matters is that when you configure your remote repositories for those packages, you
point them at the same project on GitLab.
## Why would I do this?
There are a few reasons you might want to publish all your packages to one project on GitLab:
1. You want to publish your packages on GitLab, but to a project that is different from where your code is stored.
1. You would like to group packages together in ways that make sense for your usage (all NPM packages in one project,
all packages being used by a specific department in one project, all private packages in one project, etc.)
1. You want to use one remote for all of your packages when installing them into other projects.
1. You would like to migrate your packages to a single place on GitLab from a third-party package registry and do not
want to worry about setting up separate projects for each package.
1. You want to have your CI pipelines build all of your packages to one project so the individual responsible for
validating packages can manage them all in one place.
## Example walkthrough
There is no functionality specific to this feature. All we are doing is taking advantage of functionality available in each
of the package management systems to publish packages of different types to the same place.
Let's take a look at how you might create a public place to hold all of your public packages.
### Create a project
First, create a new project on GitLab. It does not have to have any code or content. Make note of the project ID
displayed on the project overview page, as you will need this later.
### Create an access token
All of the package repositories available on the GitLab package registry are accessible using [GitLab personal access
tokens](../../profile/personal_access_tokens.md).
While using CI, you can alternatively use CI job tokens (`CI_JOB_TOKEN`) to authenticate.
### Configure your local project for the GitLab registry and upload
There are many ways to use this feature. You can upload all types of packages to the same project,
split things up based on package type, or package visibility level.
The purpose of this tutorial is to demonstrate the root idea that one project can hold many unrelated
packages, and to allow you to discover the best way to use this functionality yourself.
#### NPM
If you are using NPM, this involves creating an `.npmrc` file and adding the appropriate URL for uploading packages
to your project using your project ID, then adding a section to your `package.json` file with a similar URL.
Follow
the instructions in the [GitLab NPM Registry documentation](../npm_registry/index.md#authenticating-to-the-gitlab-npm-registry). Once
you do this, you will be able to push your NPM package to your project using `npm publish`, as described in the
[uploading packages](../npm_registry/index.md#uploading-packages) section of the docs.
#### Maven
If you are using Maven, this involves updating your `pom.xml` file with distribution sections, including the
appropriate URL for your project, as described in the [GitLab Maven Repository documentation](../maven_repository/index.md#project-level-maven-endpoint).
Then, you need to add a `settings.xml` file and [include your access token](../maven_repository/index.md#authenticating-with-a-personal-access-token).
Now you can [deploy Maven packages](../maven_repository/index.md#uploading-packages) to your project.
#### Conan
For Conan, first you need to add GitLab as a Conan registry remote. Follow the instructions in the [GitLab Conan Repository docs](../conan_repository/index.md#setting-the-conan-remote-to-the-gitlab-package-registry)
to do so. Then, create your package using the plus-separated (`+`) project path as your Conan user. For example,
if your project is located at `https://gitlab.com/foo/bar/my-proj`, then you can [create your Conan package](../conan_repository/index.md)
using `conan create . foo+bar+my-proj/channel`, where `channel` is your package channel (`stable`, `beta`, etc.). Once your package
is created, you are ready to [upload your package](../conan_repository/index.md#uploading-a-package) depending on your final package recipe. For example:
```sh
CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan upload MyPackage/1.0.0@foo+bar+my-proj/channel --all --remote=gitlab
```
export * from '@gitlab/ui';
/**
* The @gitlab/ui tooltip directive requires awkward and distracting set up in tests
* for components that use it (e.g., `attachToDocument: true` and `sync: true` passed
* to the `mount` helper from `vue-test-utils`).
*
* This mock decouples those tests from the implementation, removing the need to set
* them up specially just for these tooltips.
*/
export const GlTooltipDirective = {
bind() {},
};
export const GlTooltip = {
render(h) {
return h('div', this.$attrs, this.$slots.default);
},
};
......@@ -267,17 +267,13 @@ describe('Issue card component', () => {
});
it('renders label', () => {
const nodes = wrapper
.findAll('.badge')
.wrappers.map(label => label.attributes('data-original-title'));
const nodes = wrapper.findAll('.badge').wrappers.map(label => label.attributes('title'));
expect(nodes.includes(label1.description)).toBe(true);
});
it('sets label description as title', () => {
expect(wrapper.find('.badge').attributes('data-original-title')).toContain(
label1.description,
);
expect(wrapper.find('.badge').attributes('title')).toContain(label1.description);
});
it('sets background color of button', () => {
......
......@@ -49,7 +49,7 @@ describe('CompareVersions', () => {
const treeListBtn = wrapper.find('.js-toggle-tree-list');
expect(treeListBtn.exists()).toBe(true);
expect(treeListBtn.attributes('data-original-title')).toBe('Hide file browser');
expect(treeListBtn.attributes('title')).toBe('Hide file browser');
expect(treeListBtn.findAll(Icon).length).not.toBe(0);
expect(treeListBtn.find(Icon).props('name')).toBe('collapse-left');
});
......
......@@ -329,7 +329,7 @@ describe('DiffFileHeader component', () => {
addMergeRequestButtons: true,
});
expect(findViewFileButton().attributes('href')).toBe(viewPath);
expect(findViewFileButton().attributes('data-original-title')).toEqual(
expect(findViewFileButton().attributes('title')).toEqual(
`View file @ ${diffFile.content_sha.substr(0, 8)}`,
);
});
......
......@@ -33,7 +33,7 @@ describe('Monitoring Component', () => {
it('should render a link to environment monitoring page', () => {
expect(wrapper.attributes('href')).toEqual(monitoringUrl);
expect(findIconsByName('chart').length).toBe(1);
expect(wrapper.attributes('data-original-title')).toBe('Monitoring');
expect(wrapper.attributes('title')).toBe('Monitoring');
expect(wrapper.attributes('aria-label')).toBe('Monitoring');
});
});
......@@ -29,7 +29,7 @@ describe('Stop Component', () => {
it('should render a button to stop the environment', () => {
expect(findButton().exists()).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual('Stop environment');
expect(wrapper.attributes('title')).toEqual('Stop environment');
});
it('emits requestStopEnvironment in the event hub when button is clicked', () => {
......
......@@ -25,7 +25,7 @@ describe('Stop Component', () => {
it('should render a link to open a web terminal with the provided path', () => {
expect(wrapper.is('a')).toBe(true);
expect(wrapper.attributes('data-original-title')).toBe('Terminal');
expect(wrapper.attributes('title')).toBe('Terminal');
expect(wrapper.attributes('aria-label')).toBe('Terminal');
expect(wrapper.attributes('href')).toBe(terminalPath);
});
......
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import { createStore } from '~/ide/stores';
import * as actions from '~/ide/stores/actions/file';
import * as types from '~/ide/stores/mutation_types';
import service from '~/ide/services';
import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
import { file } from '../../helpers';
const RELATIVE_URL_ROOT = '/gitlab';
describe('IDE store file actions', () => {
let mock;
let originalGon;
let store;
beforeEach(() => {
mock = new MockAdapter(axios);
......@@ -24,12 +24,15 @@ describe('IDE store file actions', () => {
relative_url_root: RELATIVE_URL_ROOT,
};
store = createStore();
jest.spyOn(store, 'commit');
jest.spyOn(store, 'dispatch');
jest.spyOn(router, 'push').mockImplementation(() => {});
});
afterEach(() => {
mock.restore();
resetStore(store);
window.gon = originalGon;
});
......@@ -663,30 +666,33 @@ describe('IDE store file actions', () => {
});
describe('stageChange', () => {
it('calls STAGE_CHANGE with file path', done => {
testAction(
actions.stageChange,
'path',
store.state,
[
{ type: types.STAGE_CHANGE, payload: 'path' },
{ type: types.SET_LAST_COMMIT_MSG, payload: '' },
],
[],
done,
it('calls STAGE_CHANGE with file path', () => {
const f = { ...file('path'), content: 'old' };
store.state.entries[f.path] = f;
actions.stageChange(store, 'path');
expect(store.commit).toHaveBeenCalledWith(
types.STAGE_CHANGE,
expect.objectContaining({ path: 'path' }),
);
expect(store.commit).toHaveBeenCalledWith(types.SET_LAST_COMMIT_MSG, '');
});
});
describe('unstageChange', () => {
it('calls UNSTAGE_CHANGE with file path', done => {
testAction(
actions.unstageChange,
'path',
store.state,
[{ type: types.UNSTAGE_CHANGE, payload: 'path' }],
[],
done,
it('calls UNSTAGE_CHANGE with file path', () => {
const f = { ...file('path'), content: 'old' };
store.state.entries[f.path] = f;
store.state.stagedFiles.push({ f, content: 'new' });
actions.unstageChange(store, 'path');
expect(store.commit).toHaveBeenCalledWith(
types.UNSTAGE_CHANGE,
expect.objectContaining({ path: 'path' }),
);
});
});
......
import mutations from '~/ide/stores/mutations/file';
import state from '~/ide/stores/state';
import { createStore } from '~/ide/stores';
import { FILE_VIEW_MODE_PREVIEW } from '~/ide/constants';
import { file } from '../../helpers';
describe('IDE store file mutations', () => {
let localState;
let localStore;
let localFile;
beforeEach(() => {
localState = state();
localStore = createStore();
localState = localStore.state;
localFile = { ...file(), type: 'blob' };
localState.entries[localFile.path] = localFile;
......@@ -333,44 +335,154 @@ describe('IDE store file mutations', () => {
});
});
describe('STAGE_CHANGE', () => {
beforeEach(() => {
mutations.STAGE_CHANGE(localState, localFile.path);
});
describe.each`
mutationName | mutation | addedTo | removedFrom | staged | changedFilesCount | stagedFilesCount
${'STAGE_CHANGE'} | ${mutations.STAGE_CHANGE} | ${'stagedFiles'} | ${'changedFiles'} | ${true} | ${0} | ${1}
${'UNSTAGE_CHANGE'} | ${mutations.UNSTAGE_CHANGE} | ${'changedFiles'} | ${'stagedFiles'} | ${false} | ${1} | ${0}
`(
'$mutationName',
({ mutation, changedFilesCount, removedFrom, addedTo, staged, stagedFilesCount }) => {
let unstagedFile;
let stagedFile;
beforeEach(() => {
unstagedFile = {
...file('file'),
type: 'blob',
raw: 'original content',
content: 'changed content',
};
stagedFile = {
...unstagedFile,
content: 'staged content',
staged: true,
};
localState.changedFiles.push(unstagedFile);
localState.stagedFiles.push(stagedFile);
localState.entries[unstagedFile.path] = unstagedFile;
});
it('adds file into stagedFiles array', () => {
expect(localState.stagedFiles.length).toBe(1);
expect(localState.stagedFiles[0]).toEqual(localFile);
});
it('removes all changes of a file if staged and unstaged change contents are equal', () => {
unstagedFile.content = 'original content';
it('updates stagedFile if it is already staged', () => {
localFile.raw = 'testing 123';
mutation(localState, {
path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
});
mutations.STAGE_CHANGE(localState, localFile.path);
expect(localState.entries.file).toEqual(
expect.objectContaining({
content: 'original content',
staged: false,
changed: false,
}),
);
expect(localState.stagedFiles.length).toBe(1);
expect(localState.stagedFiles[0].raw).toEqual('testing 123');
});
});
expect(localState.stagedFiles.length).toBe(0);
expect(localState.changedFiles.length).toBe(0);
});
describe('UNSTAGE_CHANGE', () => {
let f;
it('removes all changes of a file if a file is deleted and a new file with same content is added', () => {
stagedFile.deleted = true;
unstagedFile.tempFile = true;
unstagedFile.content = 'original content';
beforeEach(() => {
f = { ...file(), type: 'blob', staged: true };
mutation(localState, {
path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
});
localState.stagedFiles.push(f);
localState.changedFiles.push(f);
localState.entries[f.path] = f;
});
expect(localState.stagedFiles.length).toBe(0);
expect(localState.changedFiles.length).toBe(0);
it('removes from stagedFiles array', () => {
mutations.UNSTAGE_CHANGE(localState, f.path);
expect(localState.entries.file).toEqual(
expect.objectContaining({
content: 'original content',
deleted: false,
tempFile: false,
}),
);
});
expect(localState.stagedFiles.length).toBe(0);
expect(localState.changedFiles.length).toBe(1);
});
});
it('merges deleted and added file into a changed file if the contents differ', () => {
stagedFile.deleted = true;
unstagedFile.tempFile = true;
unstagedFile.content = 'hello';
mutation(localState, {
path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
});
expect(localState.stagedFiles.length).toBe(stagedFilesCount);
expect(localState.changedFiles.length).toBe(changedFilesCount);
expect(unstagedFile).toEqual(
expect.objectContaining({
content: 'hello',
staged,
deleted: false,
tempFile: false,
changed: true,
}),
);
});
it('does not remove file from stagedFiles and changedFiles if the file was renamed, even if the contents are equal', () => {
unstagedFile.content = 'original content';
unstagedFile.prevPath = 'old_file';
mutation(localState, {
path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
});
expect(localState.entries.file).toEqual(
expect.objectContaining({
content: 'original content',
staged,
changed: false,
prevPath: 'old_file',
}),
);
expect(localState.stagedFiles.length).toBe(stagedFilesCount);
expect(localState.changedFiles.length).toBe(changedFilesCount);
});
it(`removes file from ${removedFrom} array and adds it into ${addedTo} array`, () => {
localState.stagedFiles.length = 0;
mutation(localState, {
path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
});
expect(localState.stagedFiles.length).toBe(stagedFilesCount);
expect(localState.changedFiles.length).toBe(changedFilesCount);
const f = localState.stagedFiles[0] || localState.changedFiles[0];
expect(f).toEqual(unstagedFile);
});
it(`updates file in ${addedTo} array if it is was already present in it`, () => {
unstagedFile.raw = 'testing 123';
mutation(localState, {
path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
});
expect(localState.stagedFiles.length).toBe(stagedFilesCount);
expect(localState.changedFiles.length).toBe(changedFilesCount);
const f = localState.stagedFiles[0] || localState.changedFiles[0];
expect(f.raw).toEqual('testing 123');
});
},
);
describe('TOGGLE_FILE_CHANGED', () => {
it('updates file changed status', () => {
......
......@@ -135,7 +135,7 @@ describe('Issuable suggestions suggestion component', () => {
const icon = vm.find(Icon);
expect(icon.props('name')).toBe('eye-slash');
expect(icon.attributes('data-original-title')).toBe('Confidential');
expect(icon.attributes('title')).toBe('Confidential');
});
});
});
......@@ -70,7 +70,7 @@ describe('Issuable component', () => {
const findTaskStatus = () => wrapper.find('.task-status');
const findOpenedAgoContainer = () => wrapper.find({ ref: 'openedAgoByContainer' });
const findMilestone = () => wrapper.find('.js-milestone');
const findMilestoneTooltip = () => findMilestone().attributes('data-original-title');
const findMilestoneTooltip = () => findMilestone().attributes('title');
const findDueDate = () => wrapper.find('.js-due-date');
const findLabelContainer = () => wrapper.find('.js-labels');
const findLabelLinks = () => findLabelContainer().findAll(GlLink);
......@@ -240,7 +240,7 @@ describe('Issuable component', () => {
const labels = findLabelLinks().wrappers.map(label => ({
href: label.attributes('href'),
text: label.text(),
tooltip: label.find('span').attributes('data-original-title'),
tooltip: label.find('span').attributes('title'),
}));
const expected = testLabels.map(label => ({
......
......@@ -7,8 +7,7 @@ exports[`JumpToNextDiscussionButton matches the snapshot 1`] = `
>
<button
class="btn btn-default discussion-next-btn"
data-original-title="Jump to next unresolved discussion"
title=""
title="Jump to next unresolved discussion"
>
<icon-stub
name="comment-next"
......
......@@ -30,7 +30,7 @@ describe('pipeline graph action component', () => {
});
it('should render the provided title as a bootstrap tooltip', () => {
expect(wrapper.attributes('data-original-title')).toBe('bar');
expect(wrapper.attributes('title')).toBe('bar');
});
it('should update bootstrap tooltip when title changes', done => {
......@@ -39,7 +39,7 @@ describe('pipeline graph action component', () => {
wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.attributes('data-original-title')).toBe('changed');
expect(wrapper.attributes('title')).toBe('changed');
})
.then(done)
.catch(done.fail);
......
......@@ -43,9 +43,7 @@ describe('pipeline graph job item', () => {
expect(link.attributes('href')).toBe(mockJob.status.details_path);
expect(link.attributes('data-original-title')).toEqual(
`${mockJob.name} - ${mockJob.status.label}`,
);
expect(link.attributes('title')).toEqual(`${mockJob.name} - ${mockJob.status.label}`);
expect(wrapper.find('.js-status-icon-success')).toBeDefined();
......@@ -110,9 +108,7 @@ describe('pipeline graph job item', () => {
},
});
expect(wrapper.find('.js-job-component-tooltip').attributes('data-original-title')).toBe(
'test',
);
expect(wrapper.find('.js-job-component-tooltip').attributes('title')).toBe('test');
});
it('should not render status label when it is provided', () => {
......@@ -128,7 +124,7 @@ describe('pipeline graph job item', () => {
},
});
expect(wrapper.find('.js-job-component-tooltip').attributes('data-original-title')).toEqual(
expect(wrapper.find('.js-job-component-tooltip').attributes('title')).toEqual(
'test - success',
);
});
......@@ -140,7 +136,7 @@ describe('pipeline graph job item', () => {
job: delayedJobFixture,
});
expect(wrapper.find('.js-pipeline-graph-job-link').attributes('data-original-title')).toEqual(
expect(wrapper.find('.js-pipeline-graph-job-link').attributes('title')).toEqual(
`delayed job - delayed manual action (${wrapper.vm.remainingTime})`,
);
});
......
......@@ -65,7 +65,7 @@ describe('Linked pipeline', () => {
it('should render the tooltip text as the title attribute', () => {
const tooltipRef = wrapper.find('.js-linked-pipeline-content');
const titleAttr = tooltipRef.attributes('data-original-title');
const titleAttr = tooltipRef.attributes('title');
expect(titleAttr).toContain(mockPipeline.project.name);
expect(titleAttr).toContain(mockPipeline.details.status.label);
......
......@@ -105,8 +105,6 @@ describe('Pipeline Url Component', () => {
});
expect(wrapper.find('.js-pipeline-url-failure').text()).toContain('error');
expect(wrapper.find('.js-pipeline-url-failure').attributes('data-original-title')).toContain(
'some reason',
);
expect(wrapper.find('.js-pipeline-url-failure').attributes('title')).toContain('some reason');
});
});
......@@ -86,8 +86,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
<button
class="btn input-group-text btn-secondary btn-default"
data-clipboard-text="docker login host"
data-original-title="Copy login command"
title=""
title="Copy login command"
type="button"
>
<svg
......@@ -125,8 +124,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
<button
class="btn input-group-text btn-secondary btn-default"
data-clipboard-text="docker build -t url ."
data-original-title="Copy build command"
title=""
title="Copy build command"
type="button"
>
<svg
......@@ -156,8 +154,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
<button
class="btn input-group-text btn-secondary btn-default"
data-clipboard-text="docker push url"
data-original-title="Copy push command"
title=""
title="Copy push command"
type="button"
>
<svg
......
......@@ -39,7 +39,7 @@ describe('Evidence Block', () => {
});
it('renders the correct hover text for the download', () => {
expect(wrapper.find(GlLink).attributes('data-original-title')).toBe('Download evidence JSON');
expect(wrapper.find(GlLink).attributes('title')).toBe('Download evidence JSON');
});
it('renders the correct file link for download', () => {
......@@ -63,9 +63,7 @@ describe('Evidence Block', () => {
});
it('renders the correct hover text', () => {
expect(wrapper.find(ClipboardButton).attributes('data-original-title')).toBe(
'Copy commit SHA',
);
expect(wrapper.find(ClipboardButton).attributes('title')).toBe('Copy commit SHA');
});
it('copies the sha', () => {
......
......@@ -61,7 +61,7 @@ describe('Release block milestone info', () => {
expect(milestoneLink.text()).toBe(m.title);
expect(milestoneLink.attributes('href')).toBe(m.web_url);
expect(milestoneLink.attributes('data-original-title')).toBe(m.description);
expect(milestoneLink.attributes('title')).toBe(m.description);
});
});
......
......@@ -271,7 +271,7 @@ describe('Release block', () => {
expect(milestoneLink.attributes('href')).toBe(milestone.web_url);
expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description);
expect(milestoneLink.attributes('title')).toBe(milestone.description);
});
});
......
......@@ -67,9 +67,8 @@ exports[`Repository last commit component renders commit widget 1`] = `
>
<gllink-stub
class="js-commit-pipeline"
data-original-title="Commit: failed"
href="https://test.com/pipeline"
title=""
title="Commit: failed"
>
<ciicon-stub
aria-label="Commit: failed"
......@@ -174,9 +173,8 @@ exports[`Repository last commit component renders the signature HTML as returned
>
<gllink-stub
class="js-commit-pipeline"
data-original-title="Commit: failed"
href="https://test.com/pipeline"
title=""
title="Commit: failed"
>
<ciicon-stub
aria-label="Commit: failed"
......
......@@ -178,7 +178,7 @@ describe('Assignee component', () => {
const userItems = wrapper.findAll('.user-list .user-item a');
expect(userItems.length).toBe(3);
expect(userItems.at(0).attributes('data-original-title')).toBe(users[2].name);
expect(userItems.at(0).attributes('title')).toBe(users[2].name);
});
it('passes the sorted assignees to the collapsed-assignee-list', () => {
......
......@@ -33,7 +33,7 @@ describe('AssigneeAvatarLink component', () => {
wrapper.destroy();
});
const findTooltipText = () => wrapper.attributes('data-original-title');
const findTooltipText = () => wrapper.attributes('title');
it('has the root url present in the assigneeUrl method', () => {
createComponent();
......
......@@ -25,7 +25,7 @@ describe('CollapsedAssigneeList component', () => {
const findNoUsersIcon = () => wrapper.find('i[aria-label=None]');
const findAvatarCounter = () => wrapper.find('.avatar-counter');
const findAssignees = () => wrapper.findAll(CollapsedAssignee);
const getTooltipTitle = () => wrapper.attributes('data-original-title');
const getTooltipTitle = () => wrapper.attributes('title');
afterEach(() => {
wrapper.destroy();
......
......@@ -30,7 +30,7 @@ describe('Changed file icon', () => {
const findIcon = () => wrapper.find(Icon);
const findIconName = () => findIcon().props('name');
const findIconClasses = () => findIcon().classes();
const findTooltipText = () => wrapper.attributes('data-original-title');
const findTooltipText = () => wrapper.attributes('title');
it('with isCentered true, adds center class', () => {
factory({
......@@ -89,7 +89,7 @@ describe('Changed file icon', () => {
});
it('does not have tooltip text', () => {
expect(findTooltipText()).toBe('');
expect(findTooltipText()).toBeFalsy();
});
});
......
......@@ -35,7 +35,7 @@ describe('clipboard button', () => {
});
it('should have a tooltip with default values', () => {
expect(wrapper.attributes('data-original-title')).toBe('Copy this value');
expect(wrapper.attributes('title')).toBe('Copy this value');
});
it('should render provided classname', () => {
......
......@@ -160,7 +160,7 @@ describe('Commit component', () => {
expect(refEl.attributes('href')).toBe(props.commitRef.ref_url);
expect(refEl.attributes('data-original-title')).toBe(props.commitRef.name);
expect(refEl.attributes('title')).toBe(props.commitRef.name);
expect(wrapper.find('icon-stub[name="branch"]').exists()).toBe(true);
});
......@@ -193,7 +193,7 @@ describe('Commit component', () => {
expect(refEl.attributes('href')).toBe(props.mergeRequestRef.path);
expect(refEl.attributes('data-original-title')).toBe(props.mergeRequestRef.title);
expect(refEl.attributes('title')).toBe(props.mergeRequestRef.title);
expect(wrapper.find('icon-stub[name="git-merge"]').exists()).toBe(true);
});
......
......@@ -66,7 +66,7 @@ describe('IssueAssigneesComponent', () => {
expect(findOverflowCounter().exists()).toBe(true);
expect(findOverflowCounter().text()).toEqual(expectedHidden.toString());
expect(findOverflowCounter().attributes('data-original-title')).toEqual(
expect(findOverflowCounter().attributes('title')).toEqual(
`${hiddenCount} more assignees`,
);
});
......
......@@ -25,7 +25,7 @@ describe('Time ago with tooltip component', () => {
});
const timeago = getTimeago();
expect(vm.attributes('data-original-title')).toEqual(formatDate(timestamp));
expect(vm.attributes('title')).toEqual(formatDate(timestamp));
expect(vm.text()).toEqual(timeago.format(timestamp));
});
......
......@@ -100,7 +100,7 @@ describe('User Avatar Image Component', () => {
it('does not render tooltip data attributes for on avatar image', () => {
const avatarImg = wrapper.find('img');
expect(avatarImg.attributes('data-original-title')).toBeFalsy();
expect(avatarImg.attributes('title')).toBeFalsy();
expect(avatarImg.attributes('data-placement')).not.toBeDefined();
expect(avatarImg.attributes('data-container')).not.toBeDefined();
});
......
......@@ -99,9 +99,9 @@ describe('User Avatar Link Component', () => {
});
it('should render text tooltip for <span>', () => {
expect(
wrapper.find('.js-user-avatar-link-username').attributes('data-original-title'),
).toEqual(defaultProps.tooltipText);
expect(wrapper.find('.js-user-avatar-link-username').attributes('title')).toEqual(
defaultProps.tooltipText,
);
});
it('should render text tooltip placement for <span>', () => {
......
......@@ -18,19 +18,19 @@ import axios from '~/lib/utils/axios_utils';
import { createStore } from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers';
import { file } from '../helpers';
import testAction from '../../helpers/vuex_action_helper';
import eventHub from '~/ide/eventhub';
const store = createStore();
describe('Multi-file store actions', () => {
let store;
beforeEach(() => {
spyOn(router, 'push');
});
store = createStore();
afterEach(() => {
resetStore(store);
spyOn(store, 'commit').and.callThrough();
spyOn(store, 'dispatch').and.callThrough();
spyOn(router, 'push');
});
describe('redirectToUrl', () => {
......@@ -390,58 +390,82 @@ describe('Multi-file store actions', () => {
});
});
describe('stageAllChanges', () => {
it('adds all files from changedFiles to stagedFiles', done => {
const openFile = { ...file(), path: 'test' };
describe('stage/unstageAllChanges', () => {
let file1;
let file2;
store.state.openFiles.push(openFile);
store.state.stagedFiles.push(openFile);
store.state.changedFiles.push(openFile, file('new'));
beforeEach(() => {
file1 = { ...file('test'), content: 'changed test', raw: 'test' };
file2 = { ...file('test2'), content: 'changed test2', raw: 'test2' };
testAction(
stageAllChanges,
null,
store.state,
[
{ type: types.SET_LAST_COMMIT_MSG, payload: '' },
{ type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path },
{ type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path },
],
[
{
type: 'openPendingTab',
payload: { file: openFile, keyPrefix: 'staged' },
},
],
done,
);
store.state.openFiles = [file1];
store.state.changedFiles = [file1];
store.state.stagedFiles = [{ ...file2, content: 'staged test' }];
store.state.entries = {
[file1.path]: { ...file1 },
[file2.path]: { ...file2 },
};
});
});
describe('unstageAllChanges', () => {
it('removes all files from stagedFiles after unstaging', done => {
const openFile = { ...file(), path: 'test' };
describe('stageAllChanges', () => {
it('adds all files from changedFiles to stagedFiles', () => {
stageAllChanges(store);
store.state.openFiles.push(openFile);
store.state.changedFiles.push(openFile);
store.state.stagedFiles.push(openFile, file('new'));
expect(store.commit.calls.allArgs()).toEqual([
[types.SET_LAST_COMMIT_MSG, ''],
[types.STAGE_CHANGE, jasmine.objectContaining({ path: file1.path })],
]);
});
testAction(
unstageAllChanges,
null,
store.state,
[
{ type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path },
{ type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path },
],
[
{
type: 'openPendingTab',
payload: { file: openFile, keyPrefix: 'unstaged' },
},
],
done,
);
it('opens pending tab if a change exists in that file', () => {
stageAllChanges(store);
expect(store.dispatch.calls.allArgs()).toEqual([
[
'openPendingTab',
{ file: { ...file1, staged: true, changed: true }, keyPrefix: 'staged' },
],
]);
});
it('does not open pending tab if no change exists in that file', () => {
store.state.entries[file1.path].content = 'test';
store.state.stagedFiles = [file1];
store.state.changedFiles = [store.state.entries[file1.path]];
stageAllChanges(store);
expect(store.dispatch).not.toHaveBeenCalled();
});
});
describe('unstageAllChanges', () => {
it('removes all files from stagedFiles after unstaging', () => {
unstageAllChanges(store);
expect(store.commit.calls.allArgs()).toEqual([
[types.UNSTAGE_CHANGE, jasmine.objectContaining({ path: file2.path })],
]);
});
it('opens pending tab if a change exists in that file', () => {
unstageAllChanges(store);
expect(store.dispatch.calls.allArgs()).toEqual([
['openPendingTab', { file: file1, keyPrefix: 'unstaged' }],
]);
});
it('does not open pending tab if no change exists in that file', () => {
store.state.entries[file1.path].content = 'test';
store.state.stagedFiles = [file1];
store.state.changedFiles = [store.state.entries[file1.path]];
unstageAllChanges(store);
expect(store.dispatch).not.toHaveBeenCalled();
});
});
});
......@@ -752,10 +776,6 @@ describe('Multi-file store actions', () => {
});
});
afterEach(() => {
resetStore(store);
});
it('by default renames an entry and adds to changed', done => {
testAction(
renameEntry,
......@@ -966,18 +986,19 @@ describe('Multi-file store actions', () => {
describe('error', () => {
let dispatch;
const callParams = [
{
commit() {},
state: store.state,
},
{
projectId: 'abc/def',
branchId: 'master-testing',
},
];
let callParams;
beforeEach(() => {
callParams = [
{
commit() {},
state: store.state,
},
{
projectId: 'abc/def',
branchId: 'master-testing',
},
];
dispatch = jasmine.createSpy('dispatchSpy');
document.body.innerHTML += '<div class="flash-container"></div>';
});
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册