Skip to content
Snippets Groups Projects
Commit d933bc5a authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 3f9e1b26
No related branches found
No related tags found
No related merge requests found
Showing
with 357 additions and 191 deletions
## Description of the proposal
<!--
Please describe the proposal and add a link to the source (for example, http://www.betterspecs.org/).
-->
- [ ] Mention the proposal in the next backend weekly call and the #backend channel to encourage contribution
- [ ] Proceed with the proposal once 50% of the maintainers have weighed in, and 80% of their votes are :+1:
- [ ] Once approved, mention it again in the next backend weekly call and the #backend channel
/label ~"development guidelines"
/label ~"Style decision"
/label ~documentation
/cc @gitlab-org/maintainers/rails-backend
## Description of the proposal
<!--
Please describe the proposal and add a link to the source (for example, http://www.betterspecs.org/).
-->
### Check-list
- [ ] Make sure this MR enables a static analysis check rule for new usage but
ignores current offenses
- [ ] Create a follow-up issue to fix the current offenses as a separate iteration: ISSUE_LINK
- [ ] Mention this proposal in the relevant Slack channels (e.g. `#development`, `#backend`, `#frontend`)
- [ ] If there is a choice to make between two potential styles, set up an emoji vote in the MR:
- CHOICE_A: :a:
- CHOICE_B: :b:
- Vote yourself for both choices so that people know these are the choices
- [ ] The MR doesn't have significant objections, and is getting a majority of :+1: vs :-1: (remember that [we don't need to reach a consensus](https://about.gitlab.com/handbook/values/#collaboration-is-not-consensus))
- [ ] (If applicable) One style is getting a majority of vote (compared to the other choice)
- [ ] (If applicable) Update the MR with the chosen style
- [ ] Follow the [review process](https://docs.gitlab.com/ee/development/code_review.html) as usual
- [ ] Once approved and merged by a maintainer, mention it again:
- [ ] In the relevant Slack channels (e.g. `#development`, `#backend`, `#frontend`)
- [ ] (Optional depending on the impact of the change) In the Engineering Week in Review
/label ~"Engineering Productivity" ~"Style decision" ~"development guidelines" ~"static analysis"
/cc @gitlab-org/maintainers/rails-backend
Loading
Loading
@@ -67,7 +67,7 @@ gem 'u2f', '~> 0.2.1'
gem 'validates_hostname', '~> 1.0.6'
gem 'rubyzip', '~> 1.3.0', require: 'zip'
# GitLab Pages letsencrypt support
gem 'acme-client', '~> 2.0.2'
gem 'acme-client', '~> 2.0.5'
 
# Browser detection
gem 'browser', '~> 2.5'
Loading
Loading
Loading
Loading
@@ -4,7 +4,7 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
acme-client (2.0.2)
acme-client (2.0.5)
faraday (~> 0.9, >= 0.9.1)
actioncable (5.2.3)
actionpack (= 5.2.3)
Loading
Loading
@@ -1131,7 +1131,7 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
acme-client (~> 2.0.2)
acme-client (~> 2.0.5)
activerecord-explain-analyze (~> 0.1)
acts-as-taggable-on (~> 6.0)
addressable (~> 2.7)
Loading
Loading
Loading
Loading
@@ -59,21 +59,25 @@ export default {
</script>
<template>
<div>
<div v-if="!isLocalStorageAvailable" class="dropdown-info-note">
<div v-if="!isLocalStorageAvailable" ref="localStorageNote" class="dropdown-info-note">
{{ __('This feature requires local storage to be enabled') }}
</div>
<ul v-else-if="hasItems">
<li v-for="(item, index) in processedItems" :key="`processed-items-${index}`">
<li
v-for="(item, index) in processedItems"
ref="dropdownItem"
:key="`processed-items-${index}`"
>
<button
type="button"
class="filtered-search-history-dropdown-item"
class="filtered-search-history-dropdown-item js-dropdown-button"
@click="onItemActivated(item.text)"
>
<span>
<span
v-for="(token, tokenIndex) in item.tokens"
:key="`dropdown-token-${tokenIndex}`"
class="filtered-search-history-dropdown-token"
class="filtered-search-history-dropdown-token js-dropdown-token"
>
<span class="name">{{ token.prefix }}</span>
<span class="name">{{ token.operator }}</span>
Loading
Loading
@@ -88,6 +92,7 @@ export default {
<li class="divider"></li>
<li>
<button
ref="clearButton"
type="button"
class="filtered-search-history-clear-button"
@click="onRequestClearRecentSearches($event)"
Loading
Loading
@@ -96,6 +101,8 @@ export default {
</button>
</li>
</ul>
<div v-else class="dropdown-info-note">{{ __("You don't have any recent searches") }}</div>
<div v-else ref="dropdownNote" class="dropdown-info-note">
{{ __("You don't have any recent searches") }}
</div>
</div>
</template>
import _ from 'underscore';
import { spriteIcon } from './lib/utils/common_utils';
 
const FLASH_TYPES = {
ALERT: 'alert',
NOTICE: 'notice',
SUCCESS: 'success',
WARNING: 'warning',
};
const hideFlash = (flashEl, fadeTransition = true) => {
if (fadeTransition) {
Object.assign(flashEl.style, {
Loading
Loading
@@ -59,7 +66,7 @@ const removeFlashClickListener = (flashEl, fadeTransition) => {
* additional action or link on banner next to message
*
* @param {String} message Flash message text
* @param {String} type Type of Flash, it can be `notice` or `alert` (default)
* @param {String} type Type of Flash, it can be `notice`, `success`, `warning` or `alert` (default)
* @param {Object} parent Reference to parent element under which Flash needs to appear
* @param {Object} actonConfig Map of config to show action on banner
* @param {String} href URL to which action config should point to (default: '#')
Loading
Loading
@@ -69,7 +76,7 @@ const removeFlashClickListener = (flashEl, fadeTransition) => {
*/
const createFlash = function createFlash(
message,
type = 'alert',
type = FLASH_TYPES.ALERT,
parent = document,
actionConfig = null,
fadeTransition = true,
Loading
Loading
@@ -102,5 +109,12 @@ const createFlash = function createFlash(
return flashContainer;
};
 
export { createFlash as default, createFlashEl, createAction, hideFlash, removeFlashClickListener };
export {
createFlash as default,
createFlashEl,
createAction,
hideFlash,
removeFlashClickListener,
FLASH_TYPES,
};
window.Flash = createFlash;
Loading
Loading
@@ -29,7 +29,7 @@ export default {
</script>
 
<template>
<div :class="[sizeClass, identiconBackgroundClass]" class="avatar identicon">
<div ref="identicon" :class="[sizeClass, identiconBackgroundClass]" class="avatar identicon">
{{ identiconTitle }}
</div>
</template>
Loading
Loading
@@ -65,14 +65,14 @@ export default {
<div class="issuable-note-warning">
<icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
 
<span v-if="isLockedAndConfidential">
<span v-if="isLockedAndConfidential" ref="lockedAndConfidential">
<span v-html="confidentialAndLockedDiscussionText"></span>
{{
__("People without permission will never get a notification and won't be able to comment.")
}}
</span>
 
<span v-else-if="isConfidential">
<span v-else-if="isConfidential" ref="confidential">
{{ __('This is a confidential issue.') }}
{{ __('People without permission will never get a notification.') }}
<gl-link :href="confidentialIssueDocsPath" target="_blank">
Loading
Loading
@@ -80,7 +80,7 @@ export default {
</gl-link>
</span>
 
<span v-else-if="isLocked">
<span v-else-if="isLocked" ref="locked">
{{ __('This issue is locked.') }}
{{ __('Only project members can comment.') }}
<gl-link :href="lockedIssueDocsPath" target="_blank">
Loading
Loading
Loading
Loading
@@ -47,7 +47,7 @@ export default {
:img-size="40"
/>
</div>
<div :class="{ discussion: !note.individual_note }" class="timeline-content">
<div ref="note" :class="{ discussion: !note.individual_note }" class="timeline-content">
<div class="note-header">
<div class="note-header-info">
<a :href="getUserData.path">
Loading
Loading
---
title: Update Praefect docs for subcommand
merge_request: 23255
author:
type: added
---
title: Add selective sync support to Geo Nodes API update endpoint
merge_request: 22828
author: Rajendra Kadam
type: added
---
title: Upgrade acme-client to v2.0.5
merge_request: 23498
author:
type: other
Loading
Loading
@@ -260,6 +260,14 @@ git_data_dirs({
 
For more information on Gitaly server configuration, see our [Gitaly documentation](index.md#3-gitaly-server-configuration).
 
When all Gitaly servers are configured, you can run the Praefect connection
checker to verify Praefect can connect to all Gitaly servers in the Praefect
config:
```shell
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml dial-nodes
```
#### GitLab
 
When Praefect is running, it should be exposed as a storage to GitLab. This
Loading
Loading
@@ -311,4 +319,5 @@ Here are common errors and potential causes:
- **GRPC::Unavailable (14:failed to connect to all addresses)**
- GitLab was unable to reach Praefect.
- **GRPC::Unavailable (14:all SubCons are in TransientFailure...)**
- Praefect cannot reach one or more of its child Gitaly nodes.
- Praefect cannot reach one or more of its child Gitaly nodes. Try running
the Praefect connection checker to diagnose.
Loading
Loading
@@ -84,6 +84,10 @@ Example response:
"repos_max_capacity": 25,
"container_repositories_max_capacity": 10,
"verification_max_capacity": 100,
"selective_sync_type": "namespaces",
"selective_sync_shards": [],
"selective_sync_namespace_ids": [1, 25],
"minimum_reverification_interval": 7,
"clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit",
"_links": {
Loading
Loading
@@ -104,6 +108,10 @@ Example response:
"repos_max_capacity": 25,
"container_repositories_max_capacity": 10,
"verification_max_capacity": 100,
"selective_sync_type": "namespaces",
"selective_sync_shards": [],
"selective_sync_namespace_ids": [1, 25],
"minimum_reverification_interval": 7,
"sync_object_storage": true,
"clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit",
Loading
Loading
@@ -142,6 +150,10 @@ Example response:
"repos_max_capacity": 25,
"container_repositories_max_capacity": 10,
"verification_max_capacity": 100,
"selective_sync_type": "namespaces",
"selective_sync_shards": [],
"selective_sync_namespace_ids": [1, 25],
"minimum_reverification_interval": 7,
"clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit",
"_links": {
Loading
Loading
@@ -174,6 +186,10 @@ PUT /geo_nodes/:id
| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. |
| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. |
| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. |
| `selective_sync_type` | string | no | Limit syncing to only specific groups or shards. Valid values: `"namespaces"`, `"shards"`, or `null`. |
| `selective_sync_shards` | array | no | The repository storage for the projects synced if `selective_sync_type` == `shards`. |
| `selective_sync_namespace_ids` | array | no | The IDs of groups that should be synced, if `selective_sync_type` == `namespaces`. |
| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it will be reverified. This has no effect when set on a secondary node. |
 
Example response:
 
Loading
Loading
@@ -190,6 +206,10 @@ Example response:
"repos_max_capacity": 25,
"container_repositories_max_capacity": 10,
"verification_max_capacity": 100,
"selective_sync_type": "namespaces",
"selective_sync_shards": [],
"selective_sync_namespace_ids": [1, 25],
"minimum_reverification_interval": 7,
"sync_object_storage": true,
"clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit",
Loading
Loading
Loading
Loading
@@ -660,6 +660,118 @@ GET /groups?search=foobar
]
```
 
## Hooks
Also called Group Hooks and Webhooks.
These are different from [System Hooks](system_hooks.md) that are system wide and [Project Hooks](projects.md#hooks) that are limited to one project.
### List group hooks
Get a list of group hooks
```
GET /groups/:id/hooks
```
| Attribute | Type | Required | Description |
| --------- | --------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
### Get group hook
Get a specific hook for a group.
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `hook_id` | integer | yes | The ID of a group hook |
```
GET /groups/:id/hooks/:hook_id
```
```json
{
"id": 1,
"url": "http://example.com/hook",
"group_id": 3,
"push_events": true,
"issues_events": true,
"confidential_issues_events": true,
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
"job_events": true,
"pipeline_events": true,
"wiki_page_events": true,
"enable_ssl_verification": true,
"created_at": "2012-10-12T17:04:47Z"
}
```
### Add group hook
Adds a hook to a specified group.
```
POST /groups/:id/hooks
```
| Attribute | Type | Required | Description |
| -----------------------------| -------------- | ---------| ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `url` | string | yes | The hook URL |
| `push_events` | boolean | no | Trigger hook on push events |
| `issues_events` | boolean | no | Trigger hook on issues events |
| `confidential_issues_events` | boolean | no | Trigger hook on confidential issues events |
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events |
| `job_events` | boolean | no | Trigger hook on job events |
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_page_events` | boolean | no | Trigger hook on wiki events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
### Edit group hook
Edits a hook for a specified group.
```
PUT /groups/:id/hooks/:hook_id
```
| Attribute | Type | Required | Description |
| ---------------------------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `hook_id` | integer | yes | The ID of the group hook |
| `url` | string | yes | The hook URL |
| `push_events` | boolean | no | Trigger hook on push events |
| `issues_events` | boolean | no | Trigger hook on issues events |
| `confidential_issues_events` | boolean | no | Trigger hook on confidential issues events |
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events |
| `job_events` | boolean | no | Trigger hook on job events |
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_events` | boolean | no | Trigger hook on wiki events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
### Delete group hook
Removes a hook from a group. This is an idempotent method and can be called multiple times.
Either the hook is available or not.
```
DELETE /groups/:id/hooks/:hook_id
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `hook_id` | integer | yes | The ID of the group hook. |
## Group Audit Events **(STARTER)**
 
Group audit events can be accessed via the [Group Audit Events API](audit_events.md#group-audit-events-starter)
Loading
Loading
Loading
Loading
@@ -627,7 +627,7 @@ msgstr[0] ""
msgstr[1] ""
 
msgid "1 user"
msgid_plural "%d users"
msgid_plural "%{num} users"
msgstr[0] ""
msgstr[1] ""
 
Loading
Loading
@@ -772,6 +772,9 @@ msgstr ""
msgid "A fork is a copy of a project.<br />Forking a repository allows you to make changes without affecting the original project."
msgstr ""
 
msgid "A group represents your organization in GitLab."
msgstr ""
msgid "A member of the abuse team will review your report as soon as possible."
msgstr ""
 
Loading
Loading
@@ -5406,6 +5409,9 @@ msgstr ""
msgid "Create a Mattermost team for this group"
msgstr ""
 
msgid "Create a group for your organization"
msgstr ""
msgid "Create a local proxy for storing frequently used upstream images. %{link_start}Learn more%{link_end} about dependency proxies."
msgstr ""
 
Loading
Loading
@@ -8930,6 +8936,9 @@ msgstr ""
msgid "Get a free instance review"
msgstr ""
 
msgid "Get started"
msgstr ""
msgid "Get started with error tracking"
msgstr ""
 
Loading
Loading
@@ -9395,6 +9404,9 @@ msgstr ""
msgid "Group name"
msgstr ""
 
msgid "Group name (Your organization)"
msgstr ""
msgid "Group overview"
msgstr ""
 
Loading
Loading
@@ -18463,6 +18475,9 @@ msgstr ""
msgid "Thank you for your report. A GitLab administrator will look into it shortly."
msgstr ""
 
msgid "Thanks for your purchase!"
msgstr ""
msgid "Thanks! Don't show me this again"
msgstr ""
 
Loading
Loading
@@ -21068,6 +21083,9 @@ msgstr ""
msgid "Welcome to GitLab %{name}!"
msgstr ""
 
msgid "Welcome to GitLab, %{first_name}!"
msgstr ""
msgid "Welcome to the Guided GitLab Tour"
msgstr ""
 
Loading
Loading
@@ -21382,6 +21400,9 @@ msgstr ""
msgid "You can also upload existing files from your computer using the instructions below."
msgstr ""
 
msgid "You can always edit this later"
msgstr ""
msgid "You can apply your Trial to your Personal account or create a New Group."
msgstr ""
 
Loading
Loading
@@ -21556,6 +21577,9 @@ msgstr ""
msgid "You have reached your project limit"
msgstr ""
 
msgid "You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email."
msgstr ""
msgid "You haven't added any issues to your project yet"
msgstr ""
 
Loading
Loading
@@ -21715,6 +21739,9 @@ msgstr ""
msgid "Your GPG keys (%{count})"
msgstr ""
 
msgid "Your GitLab group"
msgstr ""
msgid "Your Gitlab Gold trial will last 30 days after which point you can keep your free Gitlab account forever. We just need some additional information to activate your trial."
msgstr ""
 
Loading
Loading
Loading
Loading
@@ -40,7 +40,7 @@
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.6.2",
"@gitlab/svgs": "^1.90.0",
"@gitlab/ui": "^8.20.0",
"@gitlab/ui": "^8.21.0",
"@gitlab/visual-review-tools": "1.5.1",
"@sentry/browser": "^5.10.2",
"@sourcegraph/code-host-integration": "0.0.21",
Loading
Loading
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import eventHub from '~/filtered_search/event_hub';
import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
 
const createComponent = propsData => {
const Component = Vue.extend(RecentSearchesDropdownContent);
return new Component({
el: document.createElement('div'),
propsData,
});
};
// Remove all the newlines and whitespace from the formatted markup
const trimMarkupWhitespace = text => text.replace(/(\n|\s)+/gm, ' ').trim();
describe('RecentSearchesDropdownContent', () => {
const propsDataWithoutItems = {
items: [],
allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
};
const propsDataWithItems = {
items: ['foo', 'author:@root label:~foo bar'],
allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
describe('Recent Searches Dropdown Content', () => {
let wrapper;
const findLocalStorageNote = () => wrapper.find({ ref: 'localStorageNote' });
const findDropdownItems = () => wrapper.findAll({ ref: 'dropdownItem' });
const findDropdownNote = () => wrapper.find({ ref: 'dropdownNote' });
const createComponent = props => {
wrapper = shallowMount(RecentSearchesDropdownContent, {
propsData: {
allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
items: [],
isLocalStorageAvailable: false,
...props,
},
});
};
 
let vm;
afterEach(() => {
if (vm) {
vm.$destroy();
}
wrapper.destroy();
wrapper = null;
});
 
describe('with no items', () => {
let el;
describe('when local storage is not available', () => {
beforeEach(() => {
vm = createComponent(propsDataWithoutItems);
el = vm.$el;
createComponent();
});
 
it('should render empty state', () => {
expect(el.querySelector('.dropdown-info-note')).toBeDefined();
const items = el.querySelectorAll('.filtered-search-history-dropdown-item');
expect(items.length).toEqual(propsDataWithoutItems.items.length);
it('renders a note about enabling local storage', () => {
expect(findLocalStorageNote().exists()).toBe(true);
});
});
describe('with items', () => {
let el;
 
beforeEach(() => {
vm = createComponent(propsDataWithItems);
el = vm.$el;
it('does not render dropdown items', () => {
expect(findDropdownItems().exists()).toBe(false);
});
 
it('should render clear recent searches button', () => {
expect(el.querySelector('.filtered-search-history-clear-button')).toBeDefined();
it('does not render dropdownNote', () => {
expect(findDropdownNote().exists()).toBe(false);
});
});
 
it('should render recent search items', () => {
const items = el.querySelectorAll('.filtered-search-history-dropdown-item');
expect(items.length).toEqual(propsDataWithItems.items.length);
describe('when localStorage is available and items array is not empty', () => {
let onRecentSearchesItemSelectedSpy;
let onRequestClearRecentSearchesSpy;
 
expect(
trimMarkupWhitespace(
items[0].querySelector('.filtered-search-history-dropdown-search-token').textContent,
),
).toEqual('foo');
const item1Tokens = items[1].querySelectorAll('.filtered-search-history-dropdown-token');
expect(item1Tokens.length).toEqual(2);
expect(item1Tokens[0].querySelector('.name').textContent).toEqual('author:');
expect(item1Tokens[0].querySelector('.value').textContent).toEqual('@root');
expect(item1Tokens[1].querySelector('.name').textContent).toEqual('label:');
expect(item1Tokens[1].querySelector('.value').textContent).toEqual('~foo');
expect(
trimMarkupWhitespace(
items[1].querySelector('.filtered-search-history-dropdown-search-token').textContent,
),
).toEqual('bar');
beforeAll(() => {
onRecentSearchesItemSelectedSpy = jest.fn();
onRequestClearRecentSearchesSpy = jest.fn();
eventHub.$on('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy);
eventHub.$on('requestClearRecentSearches', onRequestClearRecentSearchesSpy);
});
});
describe('if isLocalStorageAvailable is `false`', () => {
let el;
 
beforeEach(() => {
const props = Object.assign({ isLocalStorageAvailable: false }, propsDataWithItems);
vm = createComponent(props);
el = vm.$el;
createComponent({
items: ['foo', 'author:@root label:~foo bar'],
isLocalStorageAvailable: true,
});
});
 
it('should render an info note', () => {
const note = el.querySelector('.dropdown-info-note');
const items = el.querySelectorAll('.filtered-search-history-dropdown-item');
afterAll(() => {
eventHub.$off('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy);
eventHub.$off('requestClearRecentSearchesSpy', onRequestClearRecentSearchesSpy);
});
 
expect(note).toBeDefined();
expect(note.innerText.trim()).toBe('This feature requires local storage to be enabled');
expect(items.length).toEqual(propsDataWithoutItems.items.length);
it('does not render a note about enabling local storage', () => {
expect(findLocalStorageNote().exists()).toBe(false);
});
});
 
describe('computed', () => {
describe('processedItems', () => {
it('with items', () => {
vm = createComponent(propsDataWithItems);
const { processedItems } = vm;
expect(processedItems.length).toEqual(2);
expect(processedItems[0].text).toEqual(propsDataWithItems.items[0]);
expect(processedItems[0].tokens).toEqual([]);
expect(processedItems[0].searchToken).toEqual('foo');
expect(processedItems[1].text).toEqual(propsDataWithItems.items[1]);
expect(processedItems[1].tokens.length).toEqual(2);
expect(processedItems[1].tokens[0].prefix).toEqual('author:');
expect(processedItems[1].tokens[0].suffix).toEqual('@root');
expect(processedItems[1].tokens[1].prefix).toEqual('label:');
expect(processedItems[1].tokens[1].suffix).toEqual('~foo');
expect(processedItems[1].searchToken).toEqual('bar');
});
it('does not render dropdownNote', () => {
expect(findDropdownNote().exists()).toBe(false);
});
 
it('with no items', () => {
vm = createComponent(propsDataWithoutItems);
const { processedItems } = vm;
it('renders a correct amount of dropdown items', () => {
expect(findDropdownItems()).toHaveLength(2);
});
 
expect(processedItems.length).toEqual(0);
});
it('expect second dropdown to have 2 tokens', () => {
expect(
findDropdownItems()
.at(1)
.findAll('.js-dropdown-token'),
).toHaveLength(2);
});
 
describe('hasItems', () => {
it('with items', () => {
vm = createComponent(propsDataWithItems);
const { hasItems } = vm;
it('emits recentSearchesItemSelected on dropdown item click', () => {
findDropdownItems()
.at(0)
.find('.js-dropdown-button')
.trigger('click');
 
expect(hasItems).toEqual(true);
});
expect(onRecentSearchesItemSelectedSpy).toHaveBeenCalledWith('foo');
});
 
it('with no items', () => {
vm = createComponent(propsDataWithoutItems);
const { hasItems } = vm;
it('emits requestClearRecentSearches on Clear resent searches button', () => {
wrapper.find({ ref: 'clearButton' }).trigger('click');
 
expect(hasItems).toEqual(false);
});
expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled();
});
});
 
describe('methods', () => {
describe('onItemActivated', () => {
let onRecentSearchesItemSelectedSpy;
beforeEach(() => {
onRecentSearchesItemSelectedSpy = jest.fn();
eventHub.$on('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy);
vm = createComponent(propsDataWithItems);
});
afterEach(() => {
eventHub.$off('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy);
});
it('emits event', () => {
expect(onRecentSearchesItemSelectedSpy).not.toHaveBeenCalled();
vm.onItemActivated('something');
expect(onRecentSearchesItemSelectedSpy).toHaveBeenCalledWith('something');
describe('when locale storage is available and items array is empty', () => {
beforeEach(() => {
createComponent({
isLocalStorageAvailable: true,
});
});
 
describe('onRequestClearRecentSearches', () => {
let onRequestClearRecentSearchesSpy;
beforeEach(() => {
onRequestClearRecentSearchesSpy = jest.fn();
eventHub.$on('requestClearRecentSearches', onRequestClearRecentSearchesSpy);
vm = createComponent(propsDataWithItems);
});
afterEach(() => {
eventHub.$off('requestClearRecentSearches', onRequestClearRecentSearchesSpy);
});
it('does not render a note about enabling local storage', () => {
expect(findLocalStorageNote().exists()).toBe(false);
});
 
it('emits event', () => {
expect(onRequestClearRecentSearchesSpy).not.toHaveBeenCalled();
vm.onRequestClearRecentSearches({ stopPropagation: () => {} });
it('does not render dropdown items', () => {
expect(findDropdownItems().exists()).toBe(false);
});
 
expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled();
});
it('renders dropdown note', () => {
expect(findDropdownNote().exists()).toBe(true);
});
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Code Block matches snapshot 1`] = `
<pre
class="code-block rounded"
>
<code
class="d-block"
>
test-code
</code>
</pre>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Identicon matches snapshot 1`] = `
<div
class="avatar identicon s40 bg2"
>
E
</div>
`;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment