Commit be59dd1d authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 855bf053
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
.bundle .bundle
.chef .chef
.directory .directory
.eslintcache
/.envrc /.envrc
eslint-report.html eslint-report.html
/.gitlab_shell_secret /.gitlab_shell_secret
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 12.6.0
### Fixed (32 changes, 5 of them are from the community)
- Exclude forks from Group Security Dashboard filter. !14667
- Clarify why Service Desk feature is unavailable. !19244
- Bump code quality version in template to 0.85.5. !19354
- Nullify user roles that have been accidentaly set to a value of 0. !19569
- Display CI Minutes warning only if minutes left is still below last level. !19751
- Add a unique constraint to `software_licenses.name` column. !19840
- Link user accounts to new Smartcards identities on login. !20059
- Allow valid namespace paths with dots for api PUT. !20079
- Map software license names from the v1 license scan report to an equivalent SPDX identifer. !20195
- Prefer sending external pull request pipeline statuses over general statuses to GitHub. !20364
- Abort rendering of security reports that aren't enabled. !20381
- Fix Infinite Scrolling on Environments Dashboard Project Selector. !20408
- Link user accounts to new Smartcards certificate ldap identities on login. !20470
- Handle design repositories when moving a project to a new storage. !20509
- Resolve Version dropdown goes wrong if versions are not monotonic. !20515 (Tom Quirk)
- Turn auto_complete_issues on by default. !20525
- Handle design repositories when moving existing projects to Hashed Storage. !20540
- Fix dependency metadata on the NPM registry responses. !20549
- Fix the hiding of undismissed vulnerabilities. !20599
- Fix check for existing ES limited indexing IDs. !20866
- Show actions area for fixed vulnerabilities in merge requests. !20867
- Fix typo in Kubernetes GKE setup error message. !21091
- Include projects in subgroups in group boards relative position. !21189
- Fix inability to add comments to a discussion in Design Management. !21229
- Fix Infinity % / Infinity % on Stacked Progress Bar. !21437
- Fix sort icon direction when sorting by weight. !21447 (Jan Beckmann)
- Auto-focus title text box when creating new epics. !21516 (Jan Beckmann)
- Fix analytics icon alignment. !21555
- Invalid trial form to remember user & country. !21840
- Fix styling on contribution analytics dashboard. !207012 (briankabiro)
- Add correct link to milestone in groups for issuables list after refactor.
- Show the proper message when adding a duplicate issue to an epic. (20175)
### Changed (13 changes, 1 of them is from the community)
- Make "Learn more about" links for security scanning popovers on merge request page open in new tab. !13333 (Daniel Tian)
- Redirect Admin > Settings > Geo to Admin > Geo > Settings. !19833
- Expose epic_id parameter in issues API. !19953
- Allow to login with Smartcard certificates using SAN extensions that only defines one global email identity. !20052
- Update SAST.gitlab-ci.yml - Add kubesec analyzer. !20129
- Update start trial CTA in top right banner to only appear if all namespaces are free. !20177
- Update billing page trial CTAs. !20383
- Rename software_license_policies.approval_status to software_license_policies.classification. !20414
- Add ability to edit Group Hooks. !20898
- Improve the performance of group templates finder. !20947
- Hide elasticsearch namespaces and projects when too many in rollout. !21225
- Update Explore Geo Page. !21448
- Renamed Conversational Development Index feature to DevOps Score.
### Performance (1 change)
- Do not trigger count query for pagination without count. !21232
### Added (24 changes, 2 of them are from the community)
- Add new approval rule type which allows anyone to approve. !15378
- Add Personal access token expiry policy. !17344
- Expose time logs for group issues via the GraphQL API. !18689
- Add application settings needed for soft-deletion. !18790
- Add link to new epic for promoted issues. !18839 (Jan Beckmann)
- Use issue templates on service desk(backend). !19515
- Log history for gitlab_subscriptions table. !19694
- Resolve Show plan of root group on subgroup details page. !20218
- Adjust group members API to include group SAML info. !20357
- Add user ability to append template to incoming service desk issues. !20476
- Add audit event when member access is removed due to expiration. !20529
- Update CI templates to use sitespeed 11.2.0. !20561
- Added migration for issue link types. !20617
- Add security configuration navigation item. !20711
- Create a new database composite index to support cross-project artifacts downloads. !20721
- Add deployment API updated_at filters. !20731
- Show loading spinner in design card while design is uploading. !20814
- Add most affected projects to group security dashboard. !20892
- Introduce Credentials Inventory. !20912
- Add GraphQL mutation for changing weight of an issue. !21331
- Cache vulnerability findings history endpoint for security dashboards. !21349
- Added Marginalia feature which can generate PostgreSQL query comments to Gitlab. !21364 (BalaKumar)
- Add API for states by country. !21417
- Improved trials sign up for gitlab.com. !21650
### Other (8 changes, 2 of them are from the community)
- Store and look up design management version authorship from database. !17322
- Remove redundant ManagedLicenses controller. !20131 (briankabiro)
- Updated board_service.js to use boardStore directly. !20141 (nuwe1)
- Delete any stale deploy access levels by group. !20689
- Add project webhooks limits on GitLab.com. !20730
- Remove the design_management_flag feature flag from the codebase. The feature flag toggles the Design Management feature, and has been enabled by default since 12.2. !20883
- Remove operations_feature_flags_clients.token column. !21016
- Update the alerts used in the Dependency List to follow GitLab design guidelines. !21760
## 12.5.5 ## 12.5.5
- No changes. - No changes.
......
...@@ -153,7 +153,7 @@ export default { ...@@ -153,7 +153,7 @@ export default {
), ),
{ {
startLink: startLink:
'<a href="https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.html#role-create" target="_blank" rel="noopener noreferrer">', '<a href="https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html#create-service-role" target="_blank" rel="noopener noreferrer">',
externalLinkIcon: this.externalLinkIcon, externalLinkIcon: this.externalLinkIcon,
endLink: '</a>', endLink: '</a>',
}, },
......
<script> <script>
import $ from 'jquery';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import { GlModal } from '@gitlab/ui';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
export default { export default {
components: { components: {
GlModal,
FileIcon, FileIcon,
ChangedFileIcon, ChangedFileIcon,
}, },
...@@ -17,7 +18,13 @@ export default { ...@@ -17,7 +18,13 @@ export default {
}, },
}, },
computed: { computed: {
activeButtonText() { discardModalId() {
return `discard-file-${this.activeFile.path}`;
},
discardModalTitle() {
return sprintf(__('Discard changes to %{path}?'), { path: this.activeFile.path });
},
actionButtonText() {
return this.activeFile.staged ? __('Unstage') : __('Stage'); return this.activeFile.staged ? __('Unstage') : __('Stage');
}, },
isStaged() { isStaged() {
...@@ -25,7 +32,7 @@ export default { ...@@ -25,7 +32,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['stageChange', 'unstageChange']), ...mapActions(['stageChange', 'unstageChange', 'discardFileChanges']),
actionButtonClicked() { actionButtonClicked() {
if (this.activeFile.staged) { if (this.activeFile.staged) {
this.unstageChange(this.activeFile.path); this.unstageChange(this.activeFile.path);
...@@ -34,7 +41,7 @@ export default { ...@@ -34,7 +41,7 @@ export default {
} }
}, },
showDiscardModal() { showDiscardModal() {
$(document.getElementById(`discard-file-${this.activeFile.path}`)).modal('show'); this.$refs.discardModal.show();
}, },
}, },
}; };
...@@ -53,6 +60,7 @@ export default { ...@@ -53,6 +60,7 @@ export default {
<div class="ml-auto"> <div class="ml-auto">
<button <button
v-if="!isStaged" v-if="!isStaged"
ref="discardButton"
type="button" type="button"
class="btn btn-remove btn-inverted append-right-8" class="btn btn-remove btn-inverted append-right-8"
@click="showDiscardModal" @click="showDiscardModal"
...@@ -60,6 +68,7 @@ export default { ...@@ -60,6 +68,7 @@ export default {
{{ __('Discard') }} {{ __('Discard') }}
</button> </button>
<button <button
ref="actionButton"
:class="{ :class="{
'btn-success': !isStaged, 'btn-success': !isStaged,
'btn-warning': isStaged, 'btn-warning': isStaged,
...@@ -68,8 +77,19 @@ export default { ...@@ -68,8 +77,19 @@ export default {
class="btn btn-inverted" class="btn btn-inverted"
@click="actionButtonClicked" @click="actionButtonClicked"
> >
{{ activeButtonText }} {{ actionButtonText }}
</button> </button>
</div> </div>
<gl-modal
ref="discardModal"
ok-variant="danger"
cancel-variant="light"
:ok-title="__('Discard changes')"
:modal-id="discardModalId"
:title="discardModalTitle"
@ok="discardFileChanges(activeFile.path)"
>
{{ __("You will lose all changes you've made to this file. This action cannot be undone.") }}
</gl-modal>
</div> </div>
</template> </template>
...@@ -141,7 +141,7 @@ export const getMergeRequestVersions = ( ...@@ -141,7 +141,7 @@ export const getMergeRequestVersions = (
}); });
export const openMergeRequest = ( export const openMergeRequest = (
{ dispatch, state }, { dispatch, state, getters },
{ projectId, targetProjectId, mergeRequestId } = {}, { projectId, targetProjectId, mergeRequestId } = {},
) => ) =>
dispatch('getMergeRequestData', { dispatch('getMergeRequestData', {
...@@ -152,17 +152,18 @@ export const openMergeRequest = ( ...@@ -152,17 +152,18 @@ export const openMergeRequest = (
.then(mr => { .then(mr => {
dispatch('setCurrentBranchId', mr.source_branch); dispatch('setCurrentBranchId', mr.source_branch);
// getFiles needs to be called after getting the branch data
// since files are fetched using the last commit sha of the branch
return dispatch('getBranchData', { return dispatch('getBranchData', {
projectId, projectId,
branchId: mr.source_branch, branchId: mr.source_branch,
}).then(() => }).then(() => {
dispatch('getFiles', { const branch = getters.findBranch(projectId, mr.source_branch);
return dispatch('getFiles', {
projectId, projectId,
branchId: mr.source_branch, branchId: mr.source_branch,
}), ref: branch.commit.id,
); });
});
}) })
.then(() => .then(() =>
dispatch('getMergeRequestVersions', { dispatch('getMergeRequestVersions', {
......
...@@ -111,7 +111,7 @@ export const loadFile = ({ dispatch, state }, { basePath }) => { ...@@ -111,7 +111,7 @@ export const loadFile = ({ dispatch, state }, { basePath }) => {
} }
}; };
export const loadBranch = ({ dispatch }, { projectId, branchId }) => export const loadBranch = ({ dispatch, getters }, { projectId, branchId }) =>
dispatch('getBranchData', { dispatch('getBranchData', {
projectId, projectId,
branchId, branchId,
...@@ -121,9 +121,13 @@ export const loadBranch = ({ dispatch }, { projectId, branchId }) => ...@@ -121,9 +121,13 @@ export const loadBranch = ({ dispatch }, { projectId, branchId }) =>
projectId, projectId,
branchId, branchId,
}); });
const branch = getters.findBranch(projectId, branchId);
return dispatch('getFiles', { return dispatch('getFiles', {
projectId, projectId,
branchId, branchId,
ref: branch.commit.id,
}); });
}) })
.catch(() => { .catch(() => {
......
...@@ -46,19 +46,20 @@ export const setDirectoryData = ({ state, commit }, { projectId, branchId, treeL ...@@ -46,19 +46,20 @@ export const setDirectoryData = ({ state, commit }, { projectId, branchId, treeL
}); });
}; };
export const getFiles = ({ state, commit, dispatch, getters }, { projectId, branchId } = {}) => export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const { projectId, branchId, ref = branchId } = payload;
if ( if (
!state.trees[`${projectId}/${branchId}`] || !state.trees[`${projectId}/${branchId}`] ||
(state.trees[`${projectId}/${branchId}`].tree && (state.trees[`${projectId}/${branchId}`].tree &&
state.trees[`${projectId}/${branchId}`].tree.length === 0) state.trees[`${projectId}/${branchId}`].tree.length === 0)
) { ) {
const selectedProject = state.projects[projectId]; const selectedProject = state.projects[projectId];
const selectedBranch = getters.findBranch(projectId, branchId);
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` }); commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service service
.getFiles(selectedProject.web_url, selectedBranch.commit.id) .getFiles(selectedProject.web_url, ref)
.then(({ data }) => { .then(({ data }) => {
const { entries, treeList } = decorateFiles({ const { entries, treeList } = decorateFiles({
data, data,
...@@ -77,8 +78,8 @@ export const getFiles = ({ state, commit, dispatch, getters }, { projectId, bran ...@@ -77,8 +78,8 @@ export const getFiles = ({ state, commit, dispatch, getters }, { projectId, bran
.catch(e => { .catch(e => {
dispatch('setErrorMessage', { dispatch('setErrorMessage', {
text: __('An error occurred whilst loading all the files.'), text: __('An error occurred whilst loading all the files.'),
action: payload => action: actionPayload =>
dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)), dispatch('getFiles', actionPayload).then(() => dispatch('setErrorMessage', null)),
actionText: __('Please try again'), actionText: __('Please try again'),
actionPayload: { projectId, branchId }, actionPayload: { projectId, branchId },
}); });
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import flash from './flash'; import flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion'; import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown'; import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
import { import {
parseUrlPathname, parseUrlPathname,
handleLocationHash, handleLocationHash,
...@@ -194,7 +194,7 @@ export default class MergeRequestTabs { ...@@ -194,7 +194,7 @@ export default class MergeRequestTabs {
if (!isInVueNoteablePage()) { if (!isInVueNoteablePage()) {
this.loadDiff(href); this.loadDiff(href);
} }
if (bp.getBreakpointSize() !== 'lg') { if (bp.getBreakpointSize() !== 'xl') {
this.shrinkView(); this.shrinkView();
} }
this.expandViewContainer(); this.expandViewContainer();
......
...@@ -4,11 +4,13 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; ...@@ -4,11 +4,13 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import '~/notes/index'; import '~/notes/index';
import initIssueableApp from '~/issue_show'; import initIssueableApp from '~/issue_show';
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
import initRelatedMergeRequestsApp from '~/related_merge_requests'; import initRelatedMergeRequestsApp from '~/related_merge_requests';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle'; import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
export default function() { export default function() {
initIssueableApp(); initIssueableApp();
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp(); initRelatedMergeRequestsApp();
new Issue(); // eslint-disable-line no-new new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new
......
<script>
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
export default {
name: 'SentryErrorStackTrace',
components: {
Stacktrace,
GlLoadingIcon,
},
props: {
issueStackTracePath: {
type: String,
required: true,
},
},
computed: {
...mapState('details', ['loadingStacktrace', 'stacktraceData']),
...mapGetters('details', ['stacktrace']),
},
mounted() {
this.startPollingStacktrace(this.issueStackTracePath);
},
methods: {
...mapActions('details', ['startPollingStacktrace']),
},
};
</script>
<template>
<div>
<div :class="{ 'border-bottom-0': loadingStacktrace }" class="card card-slim mt-4 mb-0">
<div class="card-header border-bottom-0">
<h5 class="card-title my-1">{{ __('Stack trace') }}</h5>
</div>
</div>
<div v-if="loadingStacktrace" class="card">
<gl-loading-icon class="py-2" label="Fetching stack trace" :size="1" />
</div>
<stacktrace v-else :entries="stacktrace" />
</div>
</template>
import Vue from 'vue';
import SentryErrorStackTrace from './components/sentry_error_stack_trace.vue';
import store from '~/error_tracking/store';
export default function initSentryErrorStacktrace() {
const sentryErrorStackTraceEl = document.querySelector('#js-sentry-error-stack-trace');
if (sentryErrorStackTraceEl) {
const { issueStackTracePath } = sentryErrorStackTraceEl.dataset;
// eslint-disable-next-line no-new
new Vue({
el: sentryErrorStackTraceEl,
components: {
SentryErrorStackTrace,
},
store,
render: createElement =>
createElement('sentry-error-stack-trace', {
props: { issueStackTracePath },
}),
});
}
}
...@@ -30,11 +30,16 @@ export default { ...@@ -30,11 +30,16 @@ export default {
}, },
computed: { computed: {
statusHtml() { statusHtml() {
if (!this.user.status) {
return '';
}
if (this.user.status.emoji && this.user.status.message_html) { if (this.user.status.emoji && this.user.status.message_html) {
return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`; return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`;
} else if (this.user.status.message_html) { } else if (this.user.status.message_html) {
return this.user.status.message_html; return this.user.status.message_html;
} }
return ''; return '';
}, },
nameIsLoading() { nameIsLoading() {
...@@ -97,7 +102,9 @@ export default { ...@@ -97,7 +102,9 @@ export default {
class="animation-container-small mb-1" class="animation-container-small mb-1"
/> />
</div> </div>
<div v-if="user.status" class="mt-2"><span v-html="statusHtml"></span></div> <div v-if="statusHtml" class="js-user-status mt-2">
<span v-html="statusHtml"></span>
</div>
</div> </div>
</div> </div>
</gl-popover> </gl-popover>
......
...@@ -71,6 +71,9 @@ ...@@ -71,6 +71,9 @@
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago') = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
- if @issue.sentry_issue.present?
#js-sentry-error-stack-trace{ data: error_details_data(@project, @issue.sentry_issue.sentry_issue_identifier) }
= render_if_exists 'projects/issues/related_issues' = render_if_exists 'projects/issues/related_issues'
#js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } } #js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
......
---
title: Add stacktrace to issue created from the sentry error detail page
merge_request: 21438
author:
type: added
---
title: 'Fix issue: Discard button in Web IDE does nothing'
merge_request: 21902
author:
type: fixed
---
title: Remove extra whitespace in user popover
merge_request: 19938
author:
type: fixed
...@@ -336,8 +336,9 @@ To create and add a new Kubernetes cluster to your project, group, or instance: ...@@ -336,8 +336,9 @@ To create and add a new Kubernetes cluster to your project, group, or instance:
- **Kubernetes cluster name** - The name you wish to give the cluster. - **Kubernetes cluster name** - The name you wish to give the cluster.
- **Environment scope** - The [associated environment](index.md#setting-the-environment-scope-premium) to this cluster. - **Environment scope** - The [associated environment](index.md#setting-the-environment-scope-premium) to this cluster.
- **Kubernetes version** - The Kubernetes version to use. Currently the only version supported is 1.14. - **Kubernetes version** - The Kubernetes version to use. Currently the only version supported is 1.14.
- **Role name** - Select the [IAM role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) - **Role name** - Select the [IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html)
to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. This IAM role is separate
to the IAM role created above, you will need to create it if it does not yet exist.
- **Region** - The [region](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html) - **Region** - The [region](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html)
in which the cluster will be created. in which the cluster will be created.
- **Key pair name** - Select the [key pair](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html) - **Key pair name** - Select the [key pair](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html)
......
...@@ -3256,6 +3256,9 @@ msgstr "" ...@@ -3256,6 +3256,9 @@ msgstr ""
msgid "Checkout|Checkout" msgid "Checkout|Checkout"
msgstr "" msgstr ""
msgid "Checkout|Edit"
msgstr ""
msgid "Cherry-pick this commit" msgid "Cherry-pick this commit"
msgstr "" msgstr ""
...@@ -4432,9 +4435,6 @@ msgstr "" ...@@ -4432,9 +4435,6 @@ msgstr ""
msgid "Code owners" msgid "Code owners"
msgstr "" msgstr ""
msgid "CodeAnalytics|Max files"
msgstr ""
msgid "CodeOwner|Pattern" msgid "CodeOwner|Pattern"
msgstr "" msgstr ""
...@@ -9513,12 +9513,6 @@ msgstr "" ...@@ -9513,12 +9513,6 @@ msgstr ""
msgid "Identifier" msgid "Identifier"
msgstr "" msgstr ""
msgid "Identify areas of the codebase associated with a lot of churn, which can indicate potential code hotspots."
msgstr ""
msgid "Identify the most frequently changed files in your repository"
msgstr ""
msgid "Identities" msgid "Identities"
msgstr "" msgstr ""
......
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
"check-dependencies": "scripts/frontend/check_dependencies.sh", "check-dependencies": "scripts/frontend/check_dependencies.sh",
"clean": "rm -rf public/assets tmp/cache/*-loader", "clean": "rm -rf public/assets tmp/cache/*-loader",
"dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'", "dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
"eslint": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .", "eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .", "eslint-fix": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .", "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .",
"file-coverage": "scripts/frontend/file_test_coverage.js", "file-coverage": "scripts/frontend/file_test_coverage.js",
"prejest": "yarn check-dependencies", "prejest": "yarn check-dependencies",
......
...@@ -7,24 +7,11 @@ describe 'Dropdown assignee', :js do ...@@ -7,24 +7,11 @@ describe 'Dropdown assignee', :js do
let!(:project) { create(:project) } let!(:project) { create(:project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') } let!(:user) { create(:user, name: 'administrator', username: 'root') }
let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_assignee) { '#js-dropdown-assignee' } let(:js_dropdown_assignee) { '#js-dropdown-assignee' }
let(:filter_dropdown) { find("#{js_dropdown_assignee} .filter-dropdown") } let(:filter_dropdown) { find("#{js_dropdown_assignee} .filter-dropdown") }
def dropdown_assignee_size
filter_dropdown.all('.filter-dropdown-item').size
end
def click_assignee(text)
find('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', text: text).click
end
before do before do
project.add_maintainer(user) project.add_maintainer(user)
project.add_maintainer(user_john)
project.add_maintainer(user_jacob)
sign_in(user) sign_in(user)
create(:issue, project: project) create(:issue, project: project)
...@@ -32,37 +19,10 @@ describe 'Dropdown assignee', :js do ...@@ -32,37 +19,10 @@ describe 'Dropdown assignee', :js do
end end
describe 'behavior' do describe 'behavior' do
it 'opens when the search bar has assignee:' do
input_filtered_search('assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'closes when the search bar is unfocused' do
find('body').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
end
it 'shows loading indicator when opened' do
slow_requests do
# We aren't using `input_filtered_search` because we want to see the loading indicator
filtered_search.set('assignee:')
expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true)
end
end
it 'hides loading indicator when loaded' do
input_filtered_search('assignee:', submit: false, extra_space: false)
expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end
it 'loads all the assignees when opened' do it 'loads all the assignees when opened' do
input_filtered_search('assignee:', submit: false, extra_space: false) input_filtered_search('assignee:', submit: false, extra_space: false)
expect(dropdown_assignee_size).to eq(4) expect_filtered_search_dropdown_results(filter_dropdown, 2)
end end
it 'shows current user at top of dropdown' do it 'shows current user at top of dropdown' do
...@@ -72,109 +32,6 @@ describe 'Dropdown assignee', :js do ...@@ -72,109 +32,6 @@ describe 'Dropdown assignee', :js do
end end
end end
describe 'filtering' do
before do
input_filtered_search('assignee:', submit: false, extra_space: false)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
end
it 'filters by name' do
input_filtered_search('jac', submit: false, extra_space: false)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name)
end
it 'filters by case insensitive name' do
input_filtered_search('JAC', submit: false, extra_space: false)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name)
end
it 'filters by username with symbol' do
input_filtered_search('@ott', submit: false, extra_space: false)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
it 'filters by case insensitive username with symbol' do
input_filtered_search('@OTT', submit: false, extra_space: false)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
it 'filters by username without symbol' do
input_filtered_search('ott', submit: false, extra_space: false)
wait_for_requests
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
it 'filters by case insensitive username without symbol' do
input_filtered_search('OTT', submit: false, extra_space: false)
wait_for_requests
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
end
describe 'selecting from dropdown' do
before do
input_filtered_search('assignee:', submit: false, extra_space: false)
end
it 'fills in the assignee username when the assignee has not been filtered' do
click_assignee(user_jacob.name)
wait_for_requests
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect_tokens([assignee_token(user_jacob.name)])
expect_filtered_search_input_empty
end
it 'fills in the assignee username when the assignee has been filtered' do
input_filtered_search('roo', submit: false, extra_space: false)
click_assignee(user.name)
wait_for_requests
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect_tokens([assignee_token(user.name)])
expect_filtered_search_input_empty
end
it 'selects `None`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'None').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect_tokens([assignee_token('None')])
expect_filtered_search_input_empty
end
it 'selects `Any`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'Any').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect_tokens([assignee_token('Any')])
expect_filtered_search_input_empty
end
end
describe 'selecting from dropdown without Ajax call' do describe 'selecting from dropdown without Ajax call' do
before do before do
Gitlab::Testing::RequestBlockerMiddleware.block_requests! Gitlab::Testing::RequestBlockerMiddleware.block_requests!
...@@ -186,59 +43,11 @@ describe 'Dropdown assignee', :js do ...@@ -186,59 +43,11 @@ describe 'Dropdown assignee', :js do
end end
it 'selects current user' do it 'selects current user' do
find('#js-dropdown-assignee .filter-dropdown-item', text: user.username).click find("#{js_dropdown_assignee} .filter-dropdown-item", text: user.username).click
expect(page).to have_css(js_dropdown_assignee, visible: false) expect(page).to have_css(js_dropdown_assignee, visible: false)
expect_tokens([assignee_token(user.username)]) expect_tokens([assignee_token(user.username)])
expect_filtered_search_input_empty expect_filtered_search_input_empty
end end
end end
describe 'input has existing content' do
it 'opens assignee dropdown with existing search term' do
input_filtered_search('searchTerm assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'opens assignee dropdown with existing author' do
input_filtered_search('author:@user assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'opens assignee dropdown with existing label' do
input_filtered_search('label:~bug assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'opens assignee dropdown with existing milestone' do
input_filtered_search('milestone:%v1.0 assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'opens assignee dropdown with existing my-reaction' do
input_filtered_search('my-reaction:star assignee:', submit: false, extra_space: false)
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
end
describe 'caching requests' do
it 'caches requests after the first load' do
input_filtered_search('assignee:', submit: false, extra_space: false)
initial_size = dropdown_assignee_size
expect(initial_size).to be > 0
new_user = create(:user)
project.add_maintainer(new_user)
find('.filtered-search-box .clear-search').click
input_filtered_search('assignee:', submit: false, extra_space: false)
expect(dropdown_assignee_size).to eq(initial_size)
end
end
end end
...@@ -7,32 +7,11 @@ describe 'Dropdown author', :js do ...@@ -7,32 +7,11 @@ describe 'Dropdown author', :js do
let!(:project) { create(:project) } let!(:project) { create(:project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') } let!(:user) { create(:user, name: 'administrator', username: 'root') }
let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
let!(:user_jacob) { create(:user, name: 'Jacob', username: 'ooter32') }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_author) { '#js-dropdown-author' } let(:js_dropdown_author) { '#js-dropdown-author' }
let(:filter_dropdown) { find("#{js_dropdown_author} .filter-dropdown") }
def send_keys_to_filtered_search(input)
input.split("").each do |i|
filtered_search.send_keys(i)
end
sleep 0.5
wait_for_requests
end
def dropdown_author_size
page.all('#js-dropdown-author .filter-dropdown .filter-dropdown-item').size
end
def click_author(text)
find('#js-dropdown-author .filter-dropdown .filter-dropdown-item', text: text).click
end
before do before do
project.add_maintainer(user) project.add_maintainer(user)
project.add_maintainer(user_john)
project.add_maintainer(user_jacob)
sign_in(user) sign_in(user)
create(:issue, project: project) create(:issue, project: project)
...@@ -40,113 +19,23 @@ describe 'Dropdown author', :js do ...@@ -40,113 +19,23 @@ describe 'Dropdown author', :js do
end end
describe 'behavior' do describe 'behavior' do
it 'opens when the search bar has author:' do
filtered_search.set('author:')
expect(page).to have_css(js_dropdown_author, visible: true)
end
it 'closes when the search bar is unfocused' do
find('body').click
expect(page).to have_css(js_dropdown_author, visible: false)
end
it 'shows loading indicator when opened' do
slow_requests do
filtered_search.set('author:')
expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true)
end
end
it 'hides loading indicator when loaded' do
send_keys_to_filtered_search('author:')
expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading')
end
it 'loads all the authors when opened' do it 'loads all the authors when opened' do
send_keys_to_filtered_search('author:') input_filtered_search('author:', submit: false, extra_space: false)
expect(dropdown_author_size).to eq(4) expect_filtered_search_dropdown_results(filter_dropdown, 2)
end end
it 'shows current user at top of dropdown' do it 'shows current user at top of dropdown' do
send_keys_to_filtered_search('author:') input_filtered_search('author:', submit: false, extra_space: false)
expect(first('#js-dropdown-author li')).to have_content(user.name) expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name)
end
end
describe 'filtering' do
before do
filtered_search.set('author')
send_keys_to_filtered_search(':')
end
it 'filters by name' do
send_keys_to_filtered_search('jac')
expect(dropdown_author_size).to eq(1)
end
it 'filters by case insensitive name' do
send_keys_to_filtered_search('Jac')
expect(dropdown_author_size).to eq(1)
end
it 'filters by username with symbol' do
send_keys_to_filtered_search('@oot')
expect(dropdown_author_size).to eq(2)
end
it 'filters by username without symbol' do
send_keys_to_filtered_search('oot')
expect(dropdown_author_size).to eq(2)
end
it 'filters by case insensitive username without symbol' do
send_keys_to_filtered_search('OOT')
expect(dropdown_author_size).to eq(2)
end
end
describe 'selecting from dropdown' do
before do
filtered_search.set('author')
send_keys_to_filtered_search(':')
end
it 'fills in the author username when the author has not been filtered' do
click_author(user_jacob.name)
wait_for_requests
expect(page).to have_css(js_dropdown_author, visible: false)
expect_tokens([author_token(user_jacob.name)])
expect_filtered_search_input_empty
end
it 'fills in the author username when the author has been filtered' do
click_author(user.name)
wait_for_requests
expect(page).to have_css(js_dropdown_author, visible: false)
expect_tokens([author_token(user.name)])
expect_filtered_search_input_empty
end end
end end
describe 'selecting from dropdown without Ajax call' do describe 'selecting from dropdown without Ajax call' do
before do before do
Gitlab::Testing::RequestBlockerMiddleware.block_requests! Gitlab::Testing::RequestBlockerMiddleware.block_requests!
filtered_search.set('author:') input_filtered_search('author:', submit: false, extra_space: false)
end end
after do after do
...@@ -154,55 +43,11 @@ describe 'Dropdown author', :js do ...@@ -154,55 +43,11 @@ describe 'Dropdown author', :js do
end end
it 'selects current user' do it 'selects current user' do
find('#js-dropdown-author .filter-dropdown-item', text: user.username).click find("#{js_dropdown_author} .filter-dropdown-item", text: user.username).click
expect(page).to have_css(js_dropdown_author, visible: false) expect(page).to have_css(js_dropdown_author, visible: false)
expect_tokens([author_token(user.username)]) expect_tokens([author_token(user.username)])
expect_filtered_search_input_empty expect_filtered_search_input_empty
end end
end end
describe 'input has existing content' do
it 'opens author dropdown with existing search term' do
filtered_search.set('searchTerm author:')
expect(page).to have_css(js_dropdown_author, visible: true)
end
it 'opens author dropdown with existing assignee' do
filtered_search.set('assignee:@user author:')
expect(page).to have_css(js_dropdown_author, visible: true)
end
it 'opens author dropdown with existing label' do
filtered_search.set('label:~bug author:')
expect(page).to have_css(js_dropdown_author, visible: true)
end
it 'opens author dropdown with existing milestone' do
filtered_search.set('milestone:%v1.0 author:')
expect(page).to have_css(js_dropdown_author, visible: true)
end
end
describe 'caching requests' do
it 'caches requests after the first load' do
filtered_search.set('author')
send_keys_to_filtered_search(':')
initial_size = dropdown_author_size
expect(initial_size).to be > 0
new_user = create(:user)
project.add_maintainer(new_user)
find('.filtered-search-box .clear-search').click
filtered_search.set('author')
send_keys_to_filtered_search(':')
expect(dropdown_author_size).to eq(initial_size)
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe 'Dropdown base', :js do
include FilteredSearchHelpers
let!(:project) { create(:project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_assignee) { '#js-dropdown-assignee' }
let(:filter_dropdown) { find("#{js_dropdown_assignee} .filter-dropdown") }
def dropdown_assignee_size
filter_dropdown.all('.filter-dropdown-item').size
end
before do
project.add_maintainer(user)
sign_in(user)
create(:issue, project: project)
visit project_issues_path(project)
end
describe 'behavior' do
it 'shows loading indicator when opened' do
slow_requests do
# We aren't using `input_filtered_search` because we want to see the loading indicator
filtered_search.set('assignee:')
expect(page).to have_css("#{js_dropdown_assignee} .filter-dropdown-loading", visible: true)
end
end
it 'hides loading indicator when loaded' do
input_filtered_search('assignee:', submit: false, extra_space: false)
expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end
end
describe 'caching requests' do
it 'caches requests after the first load' do
input_filtered_search('assignee:', submit: false, extra_space: false)
initial_size = dropdown_assignee_size
expect(initial_size).to be > 0
new_user = create(:user)
project.add_maintainer(new_user)
find('.filtered-search-box .clear-search').click
input_filtered_search('assignee:', submit: false, extra_space: false)
expect(dropdown_assignee_size).to eq(initial_size)
end
end
end
...@@ -11,30 +11,13 @@ describe 'Dropdown emoji', :js do ...@@ -11,30 +11,13 @@ describe 'Dropdown emoji', :js do
let!(:award_emoji_star) { create(:award_emoji, name: 'star', user: user, awardable: issue) } let!(:award_emoji_star) { create(:award_emoji, name: 'star', user: user, awardable: issue) }
let(:filtered_search) { find('.filtered-search') } let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_emoji) { '#js-dropdown-my-reaction' } let(:js_dropdown_emoji) { '#js-dropdown-my-reaction' }
let(:filter_dropdown) { find("#{js_dropdown_emoji} .filter-dropdown") }
def send_keys_to_filtered_search(input)
input.split("").each do |i|
filtered_search.send_keys(i)
end
sleep 0.5
wait_for_requests
end
def dropdown_emoji_size
all('gl-emoji[data-name]').size
end
def click_emoji(text)
find('#js-dropdown-my-reaction .filter-dropdown .filter-dropdown-item', text: text).click
end
before do before do
project.add_maintainer(user) project.add_maintainer(user)
create_list(:award_emoji, 2, user: user, name: 'thumbsup') create_list(:award_emoji, 2, user: user, name: 'thumbsup')
create_list(:award_emoji, 1, user: user, name: 'thumbsdown') create_list(:award_emoji, 1, user: user, name: 'thumbsdown')
create_list(:award_emoji, 3, user: user, name: 'star') create_list(:award_emoji, 3, user: user, name: 'star')
create_list(:award_emoji, 1, user: user, name: 'tea')
end end
context 'when user not logged in' do context 'when user not logged in' do
...@@ -65,137 +48,16 @@ describe 'Dropdown emoji', :js do ...@@ -65,137 +48,16 @@ describe 'Dropdown emoji', :js do
expect(page).to have_css(js_dropdown_emoji, visible: true) expect(page).to have_css(js_dropdown_emoji, visible: true)
end end
it 'closes when the search bar is unfocused' do
find('body').click
expect(page).to have_css(js_dropdown_emoji, visible: false)
end
it 'shows loading indicator when opened' do
slow_requests do
filtered_search.set('my-reaction:')
expect(page).to have_css('#js-dropdown-my-reaction .filter-dropdown-loading', visible: true)
end
end
it 'hides loading indicator when loaded' do
send_keys_to_filtered_search('my-reaction:')
expect(page).not_to have_css('#js-dropdown-my-reaction .filter-dropdown-loading')
end
it 'loads all the emojis when opened' do it 'loads all the emojis when opened' do
send_keys_to_filtered_search('my-reaction:') input_filtered_search('my-reaction:', submit: false, extra_space: false)
expect(dropdown_emoji_size).to eq(4) expect_filtered_search_dropdown_results(filter_dropdown, 3)
end end
it 'shows the most populated emoji at top of dropdown' do it 'shows the most populated emoji at top of dropdown' do
send_keys_to_filtered_search('my-reaction:') input_filtered_search('my-reaction:', submit: false, extra_space: false)
expect(first('#js-dropdown-my-reaction .filter-dropdown li')).to have_content(award_emoji_star.name)
end
end
describe 'filtering' do
before do
filtered_search.set('my-reaction')
send_keys_to_filtered_search(':')
end
it 'filters by name' do
send_keys_to_filtered_search('up')
expect(dropdown_emoji_size).to eq(1)
end
it 'filters by case insensitive name' do
send_keys_to_filtered_search('Up')
expect(dropdown_emoji_size).to eq(1)
end
end
describe 'selecting from dropdown' do
before do
filtered_search.set('my-reaction')
send_keys_to_filtered_search(':')
end
it 'selects `None`' do
find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'None').click
expect(page).to have_css(js_dropdown_emoji, visible: false)
expect_tokens([reaction_token('None', false)])
expect_filtered_search_input_empty
end
it 'selects `Any`' do
find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'Any').click
expect(page).to have_css(js_dropdown_emoji, visible: false)
expect_tokens([reaction_token('Any', false)])
expect_filtered_search_input_empty
end
it 'fills in the my-reaction name' do
click_emoji('thumbsup')
wait_for_requests
expect(page).to have_css(js_dropdown_emoji, visible: false)
expect_tokens([reaction_token('thumbsup')])
expect_filtered_search_input_empty
end
end
describe 'input has existing content' do
it 'opens my-reaction dropdown with existing search term' do
filtered_search.set('searchTerm my-reaction:')
expect(page).to have_css(js_dropdown_emoji, visible: true)
end
it 'opens my-reaction dropdown with existing assignee' do
filtered_search.set('assignee:@user my-reaction:')
expect(page).to have_css(js_dropdown_emoji, visible: true)
end
it 'opens my-reaction dropdown with existing label' do
filtered_search.set('label:~bug my-reaction:')
expect(page).to have_css(js_dropdown_emoji, visible: true)
end
it 'opens my-reaction dropdown with existing milestone' do
filtered_search.set('milestone:%v1.0 my-reaction:')
expect(page).to have_css(js_dropdown_emoji, visible: true)
end
it 'opens my-reaction dropdown with existing my-reaction' do
filtered_search.set('my-reaction:star my-reaction:')
expect(page).to have_css(js_dropdown_emoji, visible: true)
end
end
describe 'caching requests' do
it 'caches requests after the first load' do
filtered_search.set('my-reaction')
send_keys_to_filtered_search(':')
initial_size = dropdown_emoji_size
expect(initial_size).to be > 0
create_list(:award_emoji, 1, user: user, name: 'smile')
find('.filtered-search-box .clear-search').click
filtered_search.set('my-reaction')
send_keys_to_filtered_search(':')
expect(dropdown_emoji_size).to eq(initial_size) expect(first("#{js_dropdown_emoji} .filter-dropdown li")).to have_content(award_emoji_star.name)
end end
end end
end end
......
...@@ -46,9 +46,7 @@ describe 'Dropdown hint', :js do ...@@ -46,9 +46,7 @@ describe 'Dropdown hint', :js do
it 'opens when the search bar is first focused' do it 'opens when the search bar is first focused' do
expect(page).to have_css(js_dropdown_hint, visible: true) expect(page).to have_css(js_dropdown_hint, visible: true)
end
it 'closes when the search bar is unfocused' do
find('body').click find('body').click
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
...@@ -77,7 +75,7 @@ describe 'Dropdown hint', :js do ...@@ -77,7 +75,7 @@ describe 'Dropdown hint', :js do
filtered_search.click filtered_search.click
end end
it 'opens the author dropdown when you click on author' do it 'opens the token dropdown when you click on it' do
click_hint('author') click_hint('author')
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
...@@ -85,116 +83,10 @@ describe 'Dropdown hint', :js do ...@@ -85,116 +83,10 @@ describe 'Dropdown hint', :js do
expect_tokens([{ name: 'Author' }]) expect_tokens([{ name: 'Author' }])
expect_filtered_search_input_empty expect_filtered_search_input_empty
end end
it 'opens the assignee dropdown when you click on assignee' do
click_hint('assignee')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
expect_tokens([{ name: 'Assignee' }])
expect_filtered_search_input_empty
end
it 'opens the milestone dropdown when you click on milestone' do
click_hint('milestone')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
expect_tokens([{ name: 'Milestone' }])
expect_filtered_search_input_empty
end
it 'opens the release dropdown when you click on release' do
click_hint('release')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-release', visible: true)
expect_tokens([{ name: 'Release' }])
expect_filtered_search_input_empty
end
it 'opens the label dropdown when you click on label' do
click_hint('label')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
expect_tokens([{ name: 'Label' }])
expect_filtered_search_input_empty
end
it 'opens the emoji dropdown when you click on my-reaction' do
click_hint('my-reaction')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-my-reaction', visible: true)
expect_tokens([{ name: 'My-reaction' }])
expect_filtered_search_input_empty
end
it 'opens the yes-no dropdown when you click on confidential' do
click_hint('confidential')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-confidential', visible: true)
expect_tokens([{ name: 'Confidential' }])
expect_filtered_search_input_empty
end
end
describe 'selecting from dropdown with some input' do
it 'opens the author dropdown when you click on author' do
filtered_search.set('auth')
click_hint('author')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true)
expect_tokens([{ name: 'Author' }])
expect_filtered_search_input_empty
end
it 'opens the assignee dropdown when you click on assignee' do
filtered_search.set('assign')
click_hint('assignee')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
expect_tokens([{ name: 'Assignee' }])
expect_filtered_search_input_empty
end
it 'opens the milestone dropdown when you click on milestone' do
filtered_search.set('mile')
click_hint('milestone')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
expect_tokens([{ name: 'Milestone' }])
expect_filtered_search_input_empty
end
it 'opens the label dropdown when you click on label' do
filtered_search.set('lab')
click_hint('label')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
expect_tokens([{ name: 'Label' }])
expect_filtered_search_input_empty
end
it 'opens the emoji dropdown when you click on my-reaction' do
filtered_search.set('my')
click_hint('my-reaction')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-my-reaction', visible: true)
expect_tokens([{ name: 'My-reaction' }])
expect_filtered_search_input_empty
end
end end
describe 'reselecting from dropdown' do describe 'reselecting from dropdown' do
it 'reuses existing author text' do it 'reuses existing token text' do
filtered_search.send_keys('author:') filtered_search.send_keys('author:')
filtered_search.send_keys(:backspace) filtered_search.send_keys(:backspace)
filtered_search.send_keys(:backspace) filtered_search.send_keys(:backspace)
...@@ -203,63 +95,6 @@ describe 'Dropdown hint', :js do ...@@ -203,63 +95,6 @@ describe 'Dropdown hint', :js do
expect_tokens([{ name: 'Author' }]) expect_tokens([{ name: 'Author' }])
expect_filtered_search_input_empty expect_filtered_search_input_empty
end end
it 'reuses existing assignee text' do
filtered_search.send_keys('assignee:')
filtered_search.send_keys(:backspace)
filtered_search.send_keys(:backspace)
click_hint('assignee')
expect_tokens([{ name: 'Assignee' }])
expect_filtered_search_input_empty
end
it 'reuses existing milestone text' do
filtered_search.send_keys('milestone:')
filtered_search.send_keys(:backspace)
filtered_search.send_keys(:backspace)
click_hint('milestone')
expect_tokens([{ name: 'Milestone' }])
expect_filtered_search_input_empty
end
it 'reuses existing label text' do
filtered_search.send_keys('label:')
filtered_search.send_keys(:backspace)
filtered_search.send_keys(:backspace)
click_hint('label')
expect_tokens([{ name: 'Label' }])
expect_filtered_search_input_empty
end
it 'reuses existing emoji text' do
filtered_search.send_keys('my-reaction:')
filtered_search.send_keys(:backspace)
filtered_search.send_keys(:backspace)
click_hint('my-reaction')
expect_tokens([{ name: 'My-reaction' }])
expect_filtered_search_input_empty
end
end
end
context 'merge request page' do
before do
sign_in(user)
visit project_merge_requests_path(project)
filtered_search.click
end
it 'shows the WIP menu item and opens the WIP options dropdown' do
click_hint('wip')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-wip', visible: true)
expect_tokens([{ name: 'WIP' }])
expect_filtered_search_input_empty
end end
end end
end end
...@@ -10,13 +10,8 @@ describe 'Dropdown release', :js do ...@@ -10,13 +10,8 @@ describe 'Dropdown release', :js do
let!(:release) { create(:release, tag: 'v1.0', project: project) } let!(:release) { create(:release, tag: 'v1.0', project: project) }
let!(:crazy_release) { create(:release, tag: '☺!/"#%&\'{}+,-.<>;=@]_`{|}🚀', project: project) } let!(:crazy_release) { create(:release, tag: '☺!/"#%&\'{}+,-.<>;=@]_`{|}🚀', project: project) }
def filtered_search let(:filtered_search) { find('.filtered-search') }
find('.filtered-search') let(:filter_dropdown) { find('#js-dropdown-release .filter-dropdown') }
end
def filter_dropdown
find('#js-dropdown-release .filter-dropdown')
end
before do before do
project.add_maintainer(user) project.add_maintainer(user)
...@@ -31,25 +26,8 @@ describe 'Dropdown release', :js do ...@@ -31,25 +26,8 @@ describe 'Dropdown release', :js do
filtered_search.set('release:') filtered_search.set('release:')
end end
def expect_results(count)
expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: count)
end
it 'loads all the releases when opened' do it 'loads all the releases when opened' do
expect_results(2) expect_filtered_search_dropdown_results(filter_dropdown, 2)
end
it 'filters by tag name' do
filtered_search.send_keys("☺")
expect_results(1)
end
it 'fills in the release name when the autocomplete hint is clicked' do
find('#js-dropdown-release .filter-dropdown-item', text: crazy_release.tag).click
expect(page).to have_css('#js-dropdown-release', visible: false)
expect_tokens([release_token(crazy_release.tag)])
expect_filtered_search_input_empty
end end
end end
end end
...@@ -475,58 +475,6 @@ describe 'Filter issues', :js do ...@@ -475,58 +475,6 @@ describe 'Filter issues', :js do
end end
end end
describe 'RSS feeds' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
before do
group.add_developer(user)
end
shared_examples 'updates atom feed link' do |type|
it "for #{type}" do
visit path
link = find_link('Subscribe to RSS feed')
params = CGI.parse(URI.parse(link[:href]).query)
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
expected = {
'feed_token' => [user.feed_token],
'milestone_title' => [milestone.title],
'assignee_id' => [user.id.to_s]
}
expect(params).to include(expected)
expect(auto_discovery_params).to include(expected)
end
end
it_behaves_like 'updates atom feed link', :project do
let(:path) { project_issues_path(project, milestone_title: milestone.title, assignee_id: user.id) }
end
it_behaves_like 'updates atom feed link', :group do
let(:path) { issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) }
end
it 'updates atom feed link for group issues' do
visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id)
link = find('.nav-controls a[title="Subscribe to RSS feed"]', visible: false)
params = CGI.parse(URI.parse(link[:href]).query)
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
expect(params).to include('feed_token' => [user.feed_token])
expect(params).to include('milestone_title' => [milestone.title])
expect(params).to include('assignee_id' => [user.id.to_s])
expect(auto_discovery_params).to include('feed_token' => [user.feed_token])
expect(auto_discovery_params).to include('milestone_title' => [milestone.title])
expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
end
end
context 'URL has a trailing slash' do context 'URL has a trailing slash' do
before do before do
visit "#{project_issues_path(project)}/" visit "#{project_issues_path(project)}/"
......
...@@ -34,7 +34,7 @@ describe 'Visual tokens', :js do ...@@ -34,7 +34,7 @@ describe 'Visual tokens', :js do
visit project_issues_path(project) visit project_issues_path(project)
end end
describe 'editing author token' do describe 'editing a single token' do
before do before do
input_filtered_search('author:@root assignee:none', submit: false) input_filtered_search('author:@root assignee:none', submit: false)
first('.tokens-container .filtered-search-token').click first('.tokens-container .filtered-search-token').click
...@@ -42,9 +42,6 @@ describe 'Visual tokens', :js do ...@@ -42,9 +42,6 @@ describe 'Visual tokens', :js do
it 'opens author dropdown' do it 'opens author dropdown' do
expect(page).to have_css('#js-dropdown-author', visible: true) expect(page).to have_css('#js-dropdown-author', visible: true)
end
it 'makes value editable' do
expect_filtered_search_input('@root') expect_filtered_search_input('@root')
end end
...@@ -77,139 +74,6 @@ describe 'Visual tokens', :js do ...@@ -77,139 +74,6 @@ describe 'Visual tokens', :js do
end end
end end
describe 'editing assignee token' do
before do
input_filtered_search('assignee:@root author:none', submit: false)
first('.tokens-container .filtered-search-token').double_click
end
it 'opens assignee dropdown' do
expect(page).to have_css('#js-dropdown-assignee', visible: true)
end
it 'makes value editable' do
expect_filtered_search_input('@root')
end
it 'filters value' do
filtered_search.send_keys(:backspace)
expect(page).to have_css('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', count: 1)
end
it 'ends editing mode when document is clicked' do
find('#content-body').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-assignee', visible: false)
end
describe 'selecting static option from dropdown' do
before do
find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'None').click
end
it 'changes value in visual token' do
expect(first('.tokens-container .filtered-search-token .value').text).to eq('None')
end
it 'moves input to the right' do
expect(is_input_focused).to eq(true)
end
end
end
describe 'editing milestone token' do
before do
input_filtered_search('milestone:%10.0 author:none', submit: false)
first('.tokens-container .filtered-search-token').click
first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item')
end
it 'opens milestone dropdown' do
expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_ten.title)).to be_visible
expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_nine.title)).to be_visible
expect(page).to have_css('#js-dropdown-milestone', visible: true)
end
it 'selects static option from dropdown' do
find("#js-dropdown-milestone").find('.filter-dropdown-item', text: 'Upcoming').click
expect(first('.tokens-container .filtered-search-token .value').text).to eq('Upcoming')
expect(is_input_focused).to eq(true)
end
it 'makes value editable' do
expect_filtered_search_input('%10.0')
end
it 'filters value' do
filtered_search.send_keys(:backspace)
expect(page).to have_css('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', count: 1)
end
it 'ends editing mode when document is clicked' do
find('#content-body').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-milestone', visible: false)
end
end
describe 'editing label token' do
before do
input_filtered_search("label:~#{label.title} author:none", submit: false)
first('.tokens-container .filtered-search-token').double_click
first('#js-dropdown-label .filter-dropdown .filter-dropdown-item')
end
it 'opens label dropdown' do
expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
expect(page).to have_css('#js-dropdown-label', visible: true)
end
it 'selects option from dropdown' do
expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
find("#js-dropdown-label").find('.filter-dropdown-item', text: cc_label.title).click
expect(first('.tokens-container .filtered-search-token .value').text).to eq("~\"#{cc_label.title}\"")
expect(is_input_focused).to eq(true)
end
it 'makes value editable' do
expect_filtered_search_input("~#{label.title}")
end
it 'filters value' do
expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
filtered_search.send_keys(:backspace)
filter_label_dropdown.find('.filter-dropdown-item')
expect(page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size).to eq(1)
end
it 'ends editing mode when document is clicked' do
find('#content-body').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-label', visible: false)
end
it 'ends editing mode when scroll container is clicked' do
find('.scroll-container').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-label', visible: false)
end
end
describe 'editing multiple tokens' do describe 'editing multiple tokens' do
before do before do
input_filtered_search('author:@root assignee:none', submit: false) input_filtered_search('author:@root assignee:none', submit: false)
...@@ -232,10 +96,6 @@ describe 'Visual tokens', :js do ...@@ -232,10 +96,6 @@ describe 'Visual tokens', :js do
first('.tokens-container .filtered-search-term').double_click first('.tokens-container .filtered-search-term').double_click
end end
it 'opens hint dropdown' do
expect(page).to have_css('#js-dropdown-hint', visible: true)
end
it 'opens author dropdown' do it 'opens author dropdown' do
find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click
...@@ -255,59 +115,21 @@ describe 'Visual tokens', :js do ...@@ -255,59 +115,21 @@ describe 'Visual tokens', :js do
expect(page).to have_css('#js-dropdown-hint', visible: true) expect(page).to have_css('#js-dropdown-hint', visible: true)
end end
it 'opens author dropdown' do it 'opens token dropdown' do
filtered_search.send_keys('author:') filtered_search.send_keys('author:')
expect(page).to have_css('#js-dropdown-author', visible: true)
end
it 'opens assignee dropdown' do
filtered_search.send_keys('assignee:')
expect(page).to have_css('#js-dropdown-assignee', visible: true)
end
it 'opens milestone dropdown' do expect(page).to have_css('#js-dropdown-author', visible: true)
filtered_search.send_keys('milestone:')
expect(page).to have_css('#js-dropdown-milestone', visible: true)
end
it 'opens label dropdown' do
filtered_search.send_keys('label:')
expect(page).to have_css('#js-dropdown-label', visible: true)
end end
end end
describe 'creates visual tokens' do describe 'visual tokens' do
it 'creates author token' do it 'creates visual token' do
filtered_search.send_keys('author:@thomas ') filtered_search.send_keys('author:@thomas ')
token = page.all('.tokens-container .filtered-search-token')[1] token = page.all('.tokens-container .filtered-search-token')[1]
expect(token.find('.name').text).to eq('Author') expect(token.find('.name').text).to eq('Author')
expect(token.find('.value').text).to eq('@thomas') expect(token.find('.value').text).to eq('@thomas')
end end
it 'creates assignee token' do
filtered_search.send_keys('assignee:@thomas ')
token = page.all('.tokens-container .filtered-search-token')[1]
expect(token.find('.name').text).to eq('Assignee')
expect(token.find('.value').text).to eq('@thomas')
end
it 'creates milestone token' do
filtered_search.send_keys('milestone:none ')
token = page.all('.tokens-container .filtered-search-token')[1]
expect(token.find('.name').text).to eq('Milestone')
expect(token.find('.value').text).to eq('none')
end
it 'creates label token' do
filtered_search.send_keys('label:~Backend ')
token = page.all('.tokens-container .filtered-search-token')[1]
expect(token.find('.name').text).to eq('Label')
expect(token.find('.value').text).to eq('~Backend')
end
end end
it 'does not tokenize incomplete token' do it 'does not tokenize incomplete token' do
......
...@@ -3,11 +3,14 @@ ...@@ -3,11 +3,14 @@
require 'spec_helper' require 'spec_helper'
describe 'Project Issues RSS' do describe 'Project Issues RSS' do
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } let!(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, group: group, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:path) { project_issues_path(project) } let(:path) { project_issues_path(project) }
before do before do
create(:issue, project: project) create(:issue, project: project, assignees: [user])
group.add_developer(user)
end end
context 'when signed in' do context 'when signed in' do
...@@ -31,4 +34,34 @@ describe 'Project Issues RSS' do ...@@ -31,4 +34,34 @@ describe 'Project Issues RSS' do
it_behaves_like "it has an RSS button without a feed token" it_behaves_like "it has an RSS button without a feed token"
it_behaves_like "an autodiscoverable RSS feed without a feed token" it_behaves_like "an autodiscoverable RSS feed without a feed token"
end end
describe 'feeds' do
shared_examples 'updates atom feed link' do |type|
it "for #{type}" do
sign_in(user)
visit path
link = find_link('Subscribe to RSS feed')
params = CGI.parse(URI.parse(link[:href]).query)
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
expected = {
'feed_token' => [user.feed_token],
'assignee_id' => [user.id.to_s]
}
expect(params).to include(expected)
expect(auto_discovery_params).to include(expected)
end
end
it_behaves_like 'updates atom feed link', :project do
let(:path) { project_issues_path(project, assignee_id: user.id) }
end
it_behaves_like 'updates atom feed link', :group do
let(:path) { issues_group_path(group, assignee_id: user.id) }
end
end
end end
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { createStore } from '~/ide/stores';
import EditorHeader from '~/ide/components/commit_sidebar/editor_header.vue';
import { file } from '../../helpers';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('IDE commit editor header', () => {
let wrapper;
let f;
let store;
const findDiscardModal = () => wrapper.find({ ref: 'discardModal' });
const findDiscardButton = () => wrapper.find({ ref: 'discardButton' });
const findActionButton = () => wrapper.find({ ref: 'actionButton' });
beforeEach(() => {
f = file('file');
store = createStore();
wrapper = mount(EditorHeader, {
store,
localVue,
sync: false,
propsData: {
activeFile: f,
},
});
jest.spyOn(wrapper.vm, 'stageChange').mockImplementation();
jest.spyOn(wrapper.vm, 'unstageChange').mockImplementation();
jest.spyOn(wrapper.vm, 'discardFileChanges').mockImplementation();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders button to discard & stage', () => {
expect(wrapper.vm.$el.querySelectorAll('.btn').length).toBe(2);
});
describe('discard button', () => {
let modal;
beforeEach(() => {
modal = findDiscardModal();
jest.spyOn(modal.vm, 'show');
findDiscardButton().trigger('click');
});
it('opens a dialog confirming discard', () => {
expect(modal.vm.show).toHaveBeenCalled();
});
it('calls discardFileChanges if dialog result is confirmed', () => {
modal.vm.$emit('ok');
expect(wrapper.vm.discardFileChanges).toHaveBeenCalledWith(f.path);
});
});
describe('stage/unstage button', () => {
it('unstages the file if it was already staged', () => {
f.staged = true;
findActionButton().trigger('click');
expect(wrapper.vm.unstageChange).toHaveBeenCalledWith(f.path);
});
it('stages the file if it was not staged', () => {
findActionButton().trigger('click');
expect(wrapper.vm.stageChange).toHaveBeenCalledWith(f.path);
});
});
});
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import SentryErrorStackTrace from '~/sentry_error_stack_trace/components/sentry_error_stack_trace.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Sentry Error Stack Trace', () => {
let actions;
let getters;
let store;
let wrapper;
function mountComponent({
stubs = {
stacktrace: Stacktrace,
},
} = {}) {
wrapper = shallowMount(SentryErrorStackTrace, {
localVue,
stubs,
store,
propsData: {
issueStackTracePath: '/stacktrace',
},
});
}
beforeEach(() => {
actions = {
startPollingStacktrace: () => {},
};
getters = {
stacktrace: () => [{ context: [1, 2], lineNo: 53, filename: 'index.js' }],
};
const state = {
stacktraceData: {},
loadingStacktrace: true,
};
store = new Vuex.Store({
modules: {
details: {
namespaced: true,
actions,
getters,
state,
},
},
});
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('loading', () => {
it('should show spinner while loading', () => {
mountComponent();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(Stacktrace).exists()).toBe(false);
});
});
describe('Stack trace', () => {
it('should show stacktrace', () => {
store.state.details.loadingStacktrace = false;
mountComponent({ stubs: {} });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(Stacktrace).exists()).toBe(true);
});
it('should not show stacktrace if it does not exist', () => {
store.state.details.loadingStacktrace = false;
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(Stacktrace).exists()).toBe(false);
});
});
});
...@@ -29,23 +29,40 @@ describe('User Popover Component', () => { ...@@ -29,23 +29,40 @@ describe('User Popover Component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
const findUserStatus = () => wrapper.find('.js-user-status');
const findTarget = () => document.querySelector('.js-user-link');
const createWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(UserPopover, {
propsData: {
...DEFAULT_PROPS,
target: findTarget(),
...props,
},
sync: false,
...options,
});
};
describe('Empty', () => { describe('Empty', () => {
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(UserPopover, { createWrapper(
propsData: { {},
target: document.querySelector('.js-user-link'), {
user: { propsData: {
name: null, target: findTarget(),
username: null, user: {
location: null, name: null,
bio: null, username: null,
organization: null, location: null,
status: null, bio: null,
organization: null,
status: null,
},
}, },
attachToDocument: true,
}, },
attachToDocument: true, );
sync: false,
});
}); });
it('should return skeleton loaders', () => { it('should return skeleton loaders', () => {
...@@ -55,13 +72,7 @@ describe('User Popover Component', () => { ...@@ -55,13 +72,7 @@ describe('User Popover Component', () => {
describe('basic data', () => { describe('basic data', () => {
it('should show basic fields', () => { it('should show basic fields', () => {
wrapper = shallowMount(UserPopover, { createWrapper();
propsData: {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
},
sync: false,
});
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name); expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name);
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username); expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username);
...@@ -77,64 +88,38 @@ describe('User Popover Component', () => { ...@@ -77,64 +88,38 @@ describe('User Popover Component', () => {
describe('job data', () => { describe('job data', () => {
it('should show only bio if no organization is available', () => { it('should show only bio if no organization is available', () => {
const testProps = Object.assign({}, DEFAULT_PROPS); const user = { ...DEFAULT_PROPS.user, bio: 'Engineer' };
testProps.user.bio = 'Engineer';
wrapper = shallowMount(UserPopover, { createWrapper({ user });
propsData: {
...testProps,
target: document.querySelector('.js-user-link'),
},
sync: false,
});
expect(wrapper.text()).toContain('Engineer'); expect(wrapper.text()).toContain('Engineer');
}); });
it('should show only organization if no bio is available', () => { it('should show only organization if no bio is available', () => {
const testProps = Object.assign({}, DEFAULT_PROPS); const user = { ...DEFAULT_PROPS.user, organization: 'GitLab' };
testProps.user.organization = 'GitLab';
wrapper = shallowMount(UserPopover, { createWrapper({ user });
propsData: {
...testProps,
target: document.querySelector('.js-user-link'),
},
sync: false,
});
expect(wrapper.text()).toContain('GitLab'); expect(wrapper.text()).toContain('GitLab');
}); });
it('should display bio and organization in separate lines', () => { it('should display bio and organization in separate lines', () => {
const testProps = Object.assign({}, DEFAULT_PROPS); const user = { ...DEFAULT_PROPS.user, bio: 'Engineer', organization: 'GitLab' };
testProps.user.bio = 'Engineer';
testProps.user.organization = 'GitLab'; createWrapper({ user });
wrapper = shallowMount(UserPopover, {
propsData: {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
},
sync: false,
});
expect(wrapper.find('.js-bio').text()).toContain('Engineer'); expect(wrapper.find('.js-bio').text()).toContain('Engineer');
expect(wrapper.find('.js-organization').text()).toContain('GitLab'); expect(wrapper.find('.js-organization').text()).toContain('GitLab');
}); });
it('should not encode special characters in bio and organization', () => { it('should not encode special characters in bio and organization', () => {
const testProps = Object.assign({}, DEFAULT_PROPS); const user = {
testProps.user.bio = 'Manager & Team Lead'; ...DEFAULT_PROPS.user,
testProps.user.organization = 'Me & my <funky> Company'; bio: 'Manager & Team Lead',
organization: 'Me & my <funky> Company',
wrapper = shallowMount(UserPopover, { };
propsData: {
...DEFAULT_PROPS, createWrapper({ user });
target: document.querySelector('.js-user-link'),
},
sync: false,
});
expect(wrapper.find('.js-bio').text()).toContain('Manager & Team Lead'); expect(wrapper.find('.js-bio').text()).toContain('Manager & Team Lead');
expect(wrapper.find('.js-organization').text()).toContain('Me & my <funky> Company'); expect(wrapper.find('.js-organization').text()).toContain('Me & my <funky> Company');
...@@ -153,35 +138,41 @@ describe('User Popover Component', () => { ...@@ -153,35 +138,41 @@ describe('User Popover Component', () => {
describe('status data', () => { describe('status data', () => {
it('should show only message', () => { it('should show only message', () => {
const testProps = Object.assign({}, DEFAULT_PROPS); const user = { ...DEFAULT_PROPS.user, status: { message_html: 'Hello World' } };
testProps.user.status = { message_html: 'Hello World' };
wrapper = shallowMount(UserPopover, { createWrapper({ user });
propsData: {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
},
sync: false,
});
expect(findUserStatus().exists()).toBe(true);
expect(wrapper.text()).toContain('Hello World'); expect(wrapper.text()).toContain('Hello World');
}); });
it('should show message and emoji', () => { it('should show message and emoji', () => {
const testProps = Object.assign({}, DEFAULT_PROPS); const user = {
testProps.user.status = { emoji: 'basketball_player', message_html: 'Hello World' }; ...DEFAULT_PROPS.user,
status: { emoji: 'basketball_player', message_html: 'Hello World' },
wrapper = shallowMount(UserPopover, { };
propsData: {
...DEFAULT_PROPS, createWrapper({ user });
target: document.querySelector('.js-user-link'),
status: { emoji: 'basketball_player', message_html: 'Hello World' },
},
sync: false,
});
expect(findUserStatus().exists()).toBe(true);
expect(wrapper.text()).toContain('Hello World'); expect(wrapper.text()).toContain('Hello World');
expect(wrapper.html()).toContain('<gl-emoji data-name="basketball_player"'); expect(wrapper.html()).toContain('<gl-emoji data-name="basketball_player"');
}); });
it('hides the div when status is null', () => {
const user = { ...DEFAULT_PROPS.user, status: null };
createWrapper({ user });
expect(findUserStatus().exists()).toBe(false);
});
it('hides the div when status is empty', () => {
const user = { ...DEFAULT_PROPS.user, status: { emoji: '', message_html: '' } };
createWrapper({ user });
expect(findUserStatus().exists()).toBe(false);
});
}); });
}); });
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
describe('Issues Filtered Search Token Keys', () => {
describe('get', () => {
let tokenKeys;
beforeEach(() => {
tokenKeys = IssuableFilteredSearchTokenKeys.get();
});
it('should return tokenKeys', () => {
expect(tokenKeys).not.toBeNull();
});
it('should return tokenKeys as an array', () => {
expect(tokenKeys instanceof Array).toBe(true);
});
it('should always return the same array', () => {
const tokenKeys2 = IssuableFilteredSearchTokenKeys.get();
expect(tokenKeys).toEqual(tokenKeys2);
});
it('should return assignee as a string', () => {
const assignee = tokenKeys.find(tokenKey => tokenKey.key === 'assignee');
expect(assignee.type).toEqual('string');
});
});
describe('getKeys', () => {
it('should return keys', () => {
const getKeys = IssuableFilteredSearchTokenKeys.getKeys();
const keys = IssuableFilteredSearchTokenKeys.get().map(i => i.key);
keys.forEach((key, i) => {
expect(key).toEqual(getKeys[i]);
});
});
});
describe('getConditions', () => {
let conditions;
beforeEach(() => {
conditions = IssuableFilteredSearchTokenKeys.getConditions();
});
it('should return conditions', () => {
expect(conditions).not.toBeNull();
});
it('should return conditions as an array', () => {
expect(conditions instanceof Array).toBe(true);
});
});
describe('searchByKey', () => {
it('should return null when key not found', () => {
const tokenKey = IssuableFilteredSearchTokenKeys.searchByKey('notakey');
expect(tokenKey).toBeNull();
});
it('should return tokenKey when found by key', () => {
const tokenKeys = IssuableFilteredSearchTokenKeys.get();
const result = IssuableFilteredSearchTokenKeys.searchByKey(tokenKeys[0].key);
expect(result).toEqual(tokenKeys[0]);
});
});
describe('searchBySymbol', () => {
it('should return null when symbol not found', () => {
const tokenKey = IssuableFilteredSearchTokenKeys.searchBySymbol('notasymbol');
expect(tokenKey).toBeNull();
});
it('should return tokenKey when found by symbol', () => {
const tokenKeys = IssuableFilteredSearchTokenKeys.get();
const result = IssuableFilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol);
expect(result).toEqual(tokenKeys[0]);
});
});
describe('searchByKeyParam', () => {
it('should return null when key param not found', () => {
const tokenKey = IssuableFilteredSearchTokenKeys.searchByKeyParam('notakeyparam');
expect(tokenKey).toBeNull();
});
it('should return tokenKey when found by key param', () => {
const tokenKeys = IssuableFilteredSearchTokenKeys.get();
const result = IssuableFilteredSearchTokenKeys.searchByKeyParam(
`${tokenKeys[0].key}_${tokenKeys[0].param}`,
);
expect(result).toEqual(tokenKeys[0]);
});
it('should return alternative tokenKey when found by key param', () => {
const tokenKeys = IssuableFilteredSearchTokenKeys.getAlternatives();
const result = IssuableFilteredSearchTokenKeys.searchByKeyParam(
`${tokenKeys[0].key}_${tokenKeys[0].param}`,
);
expect(result).toEqual(tokenKeys[0]);
});
});
describe('searchByConditionUrl', () => {
it('should return null when condition url not found', () => {
const condition = IssuableFilteredSearchTokenKeys.searchByConditionUrl(null);
expect(condition).toBeNull();
});
it('should return condition when found by url', () => {
const conditions = IssuableFilteredSearchTokenKeys.getConditions();
const result = IssuableFilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url);
expect(result).toBe(conditions[0]);
});
});
describe('searchByConditionKeyValue', () => {
it('should return null when condition tokenKey and value not found', () => {
const condition = IssuableFilteredSearchTokenKeys.searchByConditionKeyValue(null, null);
expect(condition).toBeNull();
});
it('should return condition when found by tokenKey and value', () => {
const conditions = IssuableFilteredSearchTokenKeys.getConditions();
const result = IssuableFilteredSearchTokenKeys.searchByConditionKeyValue(
conditions[0].tokenKey,
conditions[0].value,
);
expect(result).toEqual(conditions[0]);
});
});
});
...@@ -348,6 +348,8 @@ describe('IDE store merge request actions', () => { ...@@ -348,6 +348,8 @@ describe('IDE store merge request actions', () => {
let testMergeRequest; let testMergeRequest;
let testMergeRequestChanges; let testMergeRequestChanges;
const mockGetters = { findBranch: () => ({ commit: { id: 'abcd2322' } }) };
beforeEach(() => { beforeEach(() => {
testMergeRequest = { testMergeRequest = {
source_branch: 'abcbranch', source_branch: 'abcbranch',
...@@ -406,8 +408,8 @@ describe('IDE store merge request actions', () => { ...@@ -406,8 +408,8 @@ describe('IDE store merge request actions', () => {
); );
}); });
it('dispatch actions for merge request data', done => { it('dispatches actions for merge request data', done => {
openMergeRequest(store, mr) openMergeRequest({ state: store.state, dispatch: store.dispatch, getters: mockGetters }, mr)
.then(() => { .then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([ expect(store.dispatch.calls.allArgs()).toEqual([
['getMergeRequestData', mr], ['getMergeRequestData', mr],
...@@ -424,6 +426,7 @@ describe('IDE store merge request actions', () => { ...@@ -424,6 +426,7 @@ describe('IDE store merge request actions', () => {
{ {
projectId: mr.projectId, projectId: mr.projectId,
branchId: testMergeRequest.source_branch, branchId: testMergeRequest.source_branch,
ref: 'abcd2322',
}, },
], ],
['getMergeRequestVersions', mr], ['getMergeRequestVersions', mr],
...@@ -449,7 +452,7 @@ describe('IDE store merge request actions', () => { ...@@ -449,7 +452,7 @@ describe('IDE store merge request actions', () => {
{ new_path: 'bar', path: 'bar' }, { new_path: 'bar', path: 'bar' },
]; ];
openMergeRequest(store, mr) openMergeRequest({ state: store.state, dispatch: store.dispatch, getters: mockGetters }, mr)
.then(() => { .then(() => {
expect(store.dispatch).toHaveBeenCalledWith( expect(store.dispatch).toHaveBeenCalledWith(
'updateActivityBarView', 'updateActivityBarView',
......
...@@ -285,16 +285,21 @@ describe('IDE store project actions', () => { ...@@ -285,16 +285,21 @@ describe('IDE store project actions', () => {
describe('loadBranch', () => { describe('loadBranch', () => {
const projectId = 'abc/def'; const projectId = 'abc/def';
const branchId = '123-lorem'; const branchId = '123-lorem';
const ref = 'abcd2322';
it('fetches branch data', done => { it('fetches branch data', done => {
const mockGetters = { findBranch: () => ({ commit: { id: ref } }) };
spyOn(store, 'dispatch').and.returnValue(Promise.resolve()); spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
loadBranch(store, { projectId, branchId }) loadBranch(
{ getters: mockGetters, state: store.state, dispatch: store.dispatch },
{ projectId, branchId },
)
.then(() => { .then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([ expect(store.dispatch.calls.allArgs()).toEqual([
['getBranchData', { projectId, branchId }], ['getBranchData', { projectId, branchId }],
['getMergeRequestsForBranch', { projectId, branchId }], ['getMergeRequestsForBranch', { projectId, branchId }],
['getFiles', { projectId, branchId }], ['getFiles', { projectId, branchId, ref }],
]); ]);
}) })
.then(done) .then(done)
......
...@@ -17,6 +17,7 @@ describe('Multi-file store tree actions', () => { ...@@ -17,6 +17,7 @@ describe('Multi-file store tree actions', () => {
projectId: 'abcproject', projectId: 'abcproject',
branch: 'master', branch: 'master',
branchId: 'master', branchId: 'master',
ref: '12345678',
}; };
beforeEach(() => { beforeEach(() => {
...@@ -29,14 +30,6 @@ describe('Multi-file store tree actions', () => { ...@@ -29,14 +30,6 @@ describe('Multi-file store tree actions', () => {
store.state.currentBranchId = 'master'; store.state.currentBranchId = 'master';
store.state.projects.abcproject = { store.state.projects.abcproject = {
web_url: '', web_url: '',
branches: {
master: {
workingReference: '12345678',
commit: {
id: '12345678',
},
},
},
}; };
}); });
......
...@@ -37,6 +37,10 @@ module FilteredSearchHelpers ...@@ -37,6 +37,10 @@ module FilteredSearchHelpers
filtered_search.send_keys(:enter) filtered_search.send_keys(:enter)
end end
def expect_filtered_search_dropdown_results(filter_dropdown, count)
expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: count)
end
def expect_issues_list_count(open_count, closed_count = 0) def expect_issues_list_count(open_count, closed_count = 0)
all_count = open_count + closed_count all_count = open_count + closed_count
......
...@@ -130,4 +130,26 @@ describe 'projects/issues/show' do ...@@ -130,4 +130,26 @@ describe 'projects/issues/show' do
expect(rendered).to have_selector('.status-box-open:not(.hidden)', text: 'Open') expect(rendered).to have_selector('.status-box-open:not(.hidden)', text: 'Open')
end end
end end
context 'when the issue is related to a sentry error' do
it 'renders a stack trace' do
sentry_issue = double(:sentry_issue, sentry_issue_identifier: '1066622')
allow(issue).to receive(:sentry_issue).and_return(sentry_issue)
render
expect(rendered).to have_selector(
"#js-sentry-error-stack-trace"\
"[data-issue-stack-trace-path="\
"\"/#{project.full_path}/-/error_tracking/1066622/stack_trace.json\"]"
)
end
end
context 'when the issue is not related to a sentry error' do
it 'does not render a stack trace' do
render
expect(rendered).not_to have_selector('#js-sentry-error-stack-trace')
end
end
end end
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