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

Add latest changes from gitlab-org/gitlab@master

parent 18084543
No related branches found
No related tags found
No related merge requests found
Showing
with 402 additions and 282 deletions
# frozen_string_literal: true
class DropAnalyticsRepositoryFileEditsTable < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
# Requires ExclusiveLock on the table. Not in use, no records, no FKs.
drop_table :analytics_repository_file_edits
end
def down
create_table :analytics_repository_file_edits do |t|
t.bigint :project_id, null: false
t.index :project_id
t.bigint :analytics_repository_file_id, null: false
t.date :committed_date, null: false
t.integer :num_edits, null: false, default: 0
end
add_index :analytics_repository_file_edits,
[:analytics_repository_file_id, :committed_date, :project_id],
name: 'index_file_edits_on_committed_date_file_id_and_project_id',
unique: true
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ScheduleUpdateExistingSubgroupToMatchVisibilityLevelOfParent < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'UpdateExistingSubgroupToMatchVisibilityLevelOfParent'
DELAY_INTERVAL = 5.minutes.to_i
BATCH_SIZE = 1000
VISIBILITY_LEVELS = {
internal: 10,
private: 0
}
disable_ddl_transaction!
def up
offset = update_groups(VISIBILITY_LEVELS[:internal])
update_groups(VISIBILITY_LEVELS[:private], offset: offset)
end
def down
# no-op
end
private
def update_groups(level, offset: 0)
groups = exec_query <<~SQL
SELECT id
FROM namespaces
WHERE visibility_level = #{level}
AND type = 'Group'
AND EXISTS (SELECT 1
FROM namespaces AS children
WHERE children.parent_id = namespaces.id)
SQL
ids = groups.rows.flatten
iterator = 1
ids.in_groups_of(BATCH_SIZE, false) do |batch_of_ids|
delay = DELAY_INTERVAL * (iterator + offset)
BackgroundMigrationWorker.perform_in(delay, MIGRATION, [batch_of_ids, level])
iterator += 1
end
say("Background jobs for visibility level #{level} scheduled in #{iterator} iterations")
offset + iterator
end
end
Loading
Loading
@@ -94,30 +94,6 @@ ActiveRecord::Schema.define(version: 2020_01_27_090233) do
t.index ["project_id"], name: "analytics_repository_languages_on_project_id"
end
 
create_table "analytics_repository_file_commits", force: :cascade do |t|
t.bigint "analytics_repository_file_id", null: false
t.bigint "project_id", null: false
t.date "committed_date", null: false
t.integer "commit_count", limit: 2, null: false
t.index ["analytics_repository_file_id"], name: "index_analytics_repository_file_commits_file_id"
t.index ["project_id", "committed_date", "analytics_repository_file_id"], name: "index_file_commits_on_committed_date_file_id_and_project_id", unique: true
end
create_table "analytics_repository_file_edits", force: :cascade do |t|
t.bigint "project_id", null: false
t.bigint "analytics_repository_file_id", null: false
t.date "committed_date", null: false
t.integer "num_edits", default: 0, null: false
t.index ["analytics_repository_file_id", "committed_date", "project_id"], name: "index_file_edits_on_committed_date_file_id_and_project_id", unique: true
t.index ["project_id"], name: "index_analytics_repository_file_edits_on_project_id"
end
create_table "analytics_repository_files", force: :cascade do |t|
t.bigint "project_id", null: false
t.string "file_path", limit: 4096, null: false
t.index ["project_id", "file_path"], name: "index_analytics_repository_files_on_project_id_and_file_path", unique: true
end
create_table "appearances", id: :serial, force: :cascade do |t|
t.string "title", null: false
t.text "description", null: false
Loading
Loading
@@ -4476,11 +4452,6 @@ ActiveRecord::Schema.define(version: 2020_01_27_090233) do
add_foreign_key "analytics_cycle_analytics_project_stages", "projects", on_delete: :cascade
add_foreign_key "analytics_language_trend_repository_languages", "programming_languages", on_delete: :cascade
add_foreign_key "analytics_language_trend_repository_languages", "projects", on_delete: :cascade
add_foreign_key "analytics_repository_file_commits", "analytics_repository_files", on_delete: :cascade
add_foreign_key "analytics_repository_file_commits", "projects", on_delete: :cascade
add_foreign_key "analytics_repository_file_edits", "analytics_repository_files", on_delete: :cascade
add_foreign_key "analytics_repository_file_edits", "projects", on_delete: :cascade
add_foreign_key "analytics_repository_files", "projects", on_delete: :cascade
add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify
add_foreign_key "application_settings", "namespaces", column: "instance_administrators_group_id", name: "fk_e8a145f3a7", on_delete: :nullify
add_foreign_key "application_settings", "projects", column: "file_template_project_id", name: "fk_ec757bd087", on_delete: :nullify
Loading
Loading
Loading
Loading
@@ -1062,17 +1062,36 @@ a helpful link back to how the feature was developed.
> [Introduced](<link-to-issue>) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3.
```
 
### Removing version text
Over time, version text will reference a progressively older version of GitLab. In cases where version text
refers to versions of GitLab four or more major versions back, consider removing the text.
### Importance of referencing GitLab versions and tiers
Mentioning GitLab versions and tiers is important to all users and contributors
to quickly have access to the issue or merge request that
introduced the change for reference. Also, they can easily understand what
features they have in their GitLab instance and version, given that the note has
some key information.
`[Introduced](link-to-issue) in [GitLab Premium](https://about.gitlab.com/pricing) 12.7`
links to the issue that introduced the feature, says which GitLab tier it
belongs to, says the GitLab version that it became available in, and links to
the pricing page in case the user wants to upgrade to a paid tier
to use that feature.
For example, if I'm a regular user and I'm looking at the docs for a feature I haven't used before,
I can immediately see if that feature is available to me or not. Alternatively,
if I have been using a certain feature for a long time and it changed in some way,
it's important
to me to spot when it changed and what's new in that feature.
This is even more important as we don't have a perfect process for shipping docs.
Unfortunately, we still see features without docs and docs without
features. So, for now, we cannot rely 100% on the docs site versions.
Over time, version text will reference a progressively older version of GitLab.
In cases where version text refers to versions of GitLab four or more major
versions back, you can consider removing the text if it's irrelevant or confusing.
 
For example, if the current major version is 12.x, version text referencing versions of GitLab 8.x
and older are candidates for removal.
NOTE: **Note:**
This guidance applies to any text that mentions a GitLab version, not just "Introduced in... " text.
Other text includes deprecation notices and version-specific how-to information.
and older are candidates for removal if necessary for clearer or cleaner docs.
 
## Product badges
 
Loading
Loading
@@ -1103,6 +1122,8 @@ The tier should be ideally added to headers, so that the full badge will be disp
However, it can be also mentioned from paragraphs, list items, and table cells. For these cases,
the tier mention will be represented by an orange question mark that will show the tiers on hover.
 
Use the lowest tier at the page level, even if higher-level tiers exist on the page. For example, you might have a page that is marked as Starter but a section badged as Premium.
For example:
 
- `**(STARTER)**` renders as **(STARTER)**
Loading
Loading
Loading
Loading
@@ -174,14 +174,14 @@ sequenceDiagram
c ->>+w: POST /some/url/upload
 
w->>+s: save the incoming file on a temporary location
s-->>-w:
s-->>-w: request result
 
w->>+r: POST /some/url/upload
Note over w,r: file was replaced with its location<br>and other metadata
 
opt requires async processing
r->>+redis: schedule a job
redis-->>-r:
redis-->>-r: job is scheduled
end
 
r-->>-c: request result
Loading
Loading
@@ -208,9 +208,11 @@ This is the more advanced acceleration technique we have in place.
 
Workhorse asks rails for temporary pre-signed object storage URLs and directly uploads to object storage.
 
In this setup an extra rails route needs to be implemented in order to handle authorization,
you can see an example of this in [`Projects::LfsStorageController`](https://gitlab.com/gitlab-org/gitlab/blob/cc723071ad337573e0360a879cbf99bc4fb7adb9/app/controllers/projects/lfs_storage_controller.rb)
and [its routes](https://gitlab.com/gitlab-org/gitlab/blob/cc723071ad337573e0360a879cbf99bc4fb7adb9/config/routes/git_http.rb#L31-32).
In this setup, an extra Rails route must be implemented in order to handle authorization. Examples of this can be found in:
- [`Projects::LfsStorageController`](https://gitlab.com/gitlab-org/gitlab/blob/cc723071ad337573e0360a879cbf99bc4fb7adb9/app/controllers/projects/lfs_storage_controller.rb)
and [its routes](https://gitlab.com/gitlab-org/gitlab/blob/cc723071ad337573e0360a879cbf99bc4fb7adb9/config/routes/git_http.rb#L31-32).
- [API endpoints for uploading packages](packages.md#file-uploads).
 
**note:** this will fallback to _disk buffered upload_ when `direct_upload` is disabled inside the [object storage setting](../administration/uploads.md#object-storage-settings).
The answer to the `/authorize` call will only contain a file system path.
Loading
Loading
@@ -231,17 +233,17 @@ sequenceDiagram
 
w->>+os: PUT file
Note over w,os: file is stored on a temporary location. Rails select the destination
os-->>-w:
os-->>-w: request result
 
w->>+r: POST /some/url/upload
Note over w,r: file was replaced with its location<br>and other metadata
 
r->>+os: move object to final destination
os-->>-r:
os-->>-r: request result
 
opt requires async processing
r->>+redis: schedule a job
redis-->>-r:
redis-->>-r: job is scheduled
end
 
r-->>-c: request result
Loading
Loading
Loading
Loading
@@ -9,4 +9,7 @@ You can import your existing repositories by providing the Git URL:
1. Click **Create project** to begin the import process
1. Once complete, you will be redirected to your newly created project
 
NOTE: **Note:**
If your password has special characters, you will need to enter them URL encoded, please see the [GitLab issue](https://gitlab.com/gitlab-org/gitlab/issues/29952) for more information.
![Import project by repo URL](img/import_projects_from_repo_url.png)
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# This background migration updates children of group to match visibility of a parent
class UpdateExistingSubgroupToMatchVisibilityLevelOfParent
def perform(parents_groups_ids, level)
groups_ids = Gitlab::ObjectHierarchy.new(Group.where(id: parents_groups_ids))
.base_and_descendants
.where("visibility_level > ?", level)
.select(:id)
return if groups_ids.empty?
Group
.where(id: groups_ids)
.update_all(visibility_level: level)
end
end
end
end
Loading
Loading
@@ -147,11 +147,11 @@ module Gitlab
end
 
def current_lock_timeout_in_ms
timing_configuration[current_iteration - 1][0].in_milliseconds
Integer(timing_configuration[current_iteration - 1][0].in_milliseconds)
end
 
def current_sleep_time_in_seconds
timing_configuration[current_iteration - 1][1].to_i
timing_configuration[current_iteration - 1][1].to_f
end
end
end
Loading
Loading
Loading
Loading
@@ -10431,6 +10431,9 @@ msgstr ""
msgid "Invalid date format. Please use UTC format as YYYY-MM-DD"
msgstr ""
 
msgid "Invalid date range"
msgstr ""
msgid "Invalid feature"
msgstr ""
 
Loading
Loading
import { shallowMount } from '@vue/test-utils';
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import { GlFormInputGroup } from '@gitlab/ui';
describe('Blob Embeddable', () => {
let wrapper;
const url = 'https://foo.bar';
function createComponent() {
wrapper = shallowMount(BlobEmbeddable, {
propsData: {
url,
},
});
}
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders gl-form-input-group component', () => {
expect(wrapper.find(GlFormInputGroup).exists()).toBe(true);
});
it('makes up optionValues based on the url prop', () => {
expect(wrapper.vm.optionValues).toEqual([
{ name: 'Embed', value: expect.stringContaining(`${url}.js`) },
{ name: 'Share', value: url },
]);
});
});
Loading
Loading
@@ -69,9 +69,8 @@ exports[`Dashboard template matches the default snapshot 1`] = `
label-size="sm"
>
<date-time-picker-stub
end="2020-01-01T18:57:47.000Z"
start="2020-01-01T18:27:47.000Z"
timewindows="[object Object]"
options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
value="[object Object]"
/>
</gl-form-group-stub>
Loading
Loading
Loading
Loading
@@ -5,13 +5,7 @@ import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import { propsData } from '../init_utils';
 
jest.mock('~/lib/utils/url_utility', () => ({
getParameterValues: jest.fn().mockImplementation(param => {
if (param === 'start') return ['2020-01-01T18:27:47.000Z'];
if (param === 'end') return ['2020-01-01T18:57:47.000Z'];
return [];
}),
}));
jest.mock('~/lib/utils/url_utility');
 
describe('Dashboard template', () => {
let wrapper;
Loading
Loading
import { mount } from '@vue/test-utils';
import createFlash from '~/flash';
import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import { propsData } from '../init_utils';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
getParameterValues: jest.fn().mockReturnValue('<script>alert("XSS")</script>'),
}));
describe('dashboard invalid url parameters', () => {
let store;
let wrapper;
let mock;
const createMountedWrapper = (props = {}, options = {}) => {
wrapper = mount(Dashboard, {
propsData: { ...propsData, ...props },
store,
...options,
});
};
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
mock.restore();
});
it('shows an error message if invalid url parameters are passed', done => {
createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
wrapper.vm
.$nextTick()
.then(() => {
expect(createFlash).toHaveBeenCalled();
done();
})
.catch(done.fail);
});
});
import { mount } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import { propsData, setupComponentStore } from '../init_utils';
import { metricsDashboardPayload, mockApiEndpoint } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterValues: jest.fn().mockImplementation(param => {
if (param === 'start') return ['2019-10-01T18:27:47.000Z'];
if (param === 'end') return ['2019-10-01T18:57:47.000Z'];
return [];
}),
mergeUrlParams: jest.fn().mockReturnValue('#'),
}));
describe('dashboard time window', () => {
let store;
let wrapper;
let mock;
const createComponentWrapperMounted = (props = {}, options = {}) => {
wrapper = mount(Dashboard, {
propsData: { ...propsData, ...props },
store,
...options,
});
};
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
mock.restore();
});
it('shows an active quick range option', done => {
mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsDashboardPayload);
createComponentWrapperMounted({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
setupComponentStore(wrapper);
wrapper.vm
.$nextTick()
.then(() => {
const timeWindowDropdownItems = wrapper
.find({ ref: 'dateTimePicker' })
.findAll(GlDropdownItem);
const activeItem = timeWindowDropdownItems.wrappers.filter(itemWrapper =>
itemWrapper.find('.active').exists(),
);
expect(activeItem.length).toBe(1);
done();
})
.catch(done.fail);
});
});
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import createFlash from '~/flash';
import { queryToObject, redirectTo, removeParams, mergeUrlParams } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
import { mockProjectDir } from '../mock_data';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import { propsData } from '../init_utils';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
describe('dashboard invalid url parameters', () => {
let store;
let wrapper;
let mock;
const fetchDataMock = jest.fn();
const createMountedWrapper = (props = { hasMetrics: true }, options = {}) => {
wrapper = mount(Dashboard, {
propsData: { ...propsData, ...props },
store,
stubs: ['graph-group', 'panel-type'],
methods: {
fetchData: fetchDataMock,
},
...options,
});
};
const findDateTimePicker = () => wrapper.find({ ref: 'dateTimePicker' });
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
mock.restore();
fetchDataMock.mockReset();
queryToObject.mockReset();
});
it('passes default url parameters to the time range picker', () => {
queryToObject.mockReturnValue({});
createMountedWrapper();
return wrapper.vm.$nextTick().then(() => {
expect(findDateTimePicker().props('value')).toMatchObject({
duration: { seconds: 28800 },
});
expect(fetchDataMock).toHaveBeenCalledTimes(1);
expect(fetchDataMock).toHaveBeenCalledWith({
start: expect.any(String),
end: expect.any(String),
});
});
});
it('passes a fixed time range in the URL to the time range picker', () => {
const params = {
start: '2019-01-01T00:00:00.000Z',
end: '2019-01-10T00:00:00.000Z',
};
queryToObject.mockReturnValue(params);
createMountedWrapper();
return wrapper.vm.$nextTick().then(() => {
expect(findDateTimePicker().props('value')).toEqual(params);
expect(fetchDataMock).toHaveBeenCalledTimes(1);
expect(fetchDataMock).toHaveBeenCalledWith(params);
});
});
it('passes a rolling time range in the URL to the time range picker', () => {
queryToObject.mockReturnValue({
duration_seconds: '120',
});
createMountedWrapper();
return wrapper.vm.$nextTick().then(() => {
expect(findDateTimePicker().props('value')).toMatchObject({
duration: { seconds: 60 * 2 },
});
expect(fetchDataMock).toHaveBeenCalledTimes(1);
expect(fetchDataMock).toHaveBeenCalledWith({
start: expect.any(String),
end: expect.any(String),
});
});
});
it('shows an error message and loads a default time range if invalid url parameters are passed', () => {
queryToObject.mockReturnValue({
start: '<script>alert("XSS")</script>',
end: '<script>alert("XSS")</script>',
});
createMountedWrapper();
return wrapper.vm.$nextTick().then(() => {
expect(createFlash).toHaveBeenCalled();
expect(findDateTimePicker().props('value')).toMatchObject({
duration: { seconds: 28800 },
});
expect(fetchDataMock).toHaveBeenCalledTimes(1);
expect(fetchDataMock).toHaveBeenCalledWith({
start: expect.any(String),
end: expect.any(String),
});
});
});
it('redirects to different time range', () => {
const toUrl = `${mockProjectDir}/-/environments/1/metrics`;
removeParams.mockReturnValueOnce(toUrl);
createMountedWrapper();
return wrapper.vm.$nextTick().then(() => {
findDateTimePicker().vm.$emit('input', {
duration: { seconds: 120 },
});
// redirect to plus + new parameters
expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: '120' }, toUrl);
expect(redirectTo).toHaveBeenCalledTimes(1);
});
});
});
Loading
Loading
@@ -90,11 +90,10 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
type="button"
>
<svg
aria-hidden="true"
class="s16 ic-duplicate"
class="gl-icon s16"
>
<use
xlink:href="#duplicate"
href="#copy-to-clipboard"
/>
</svg>
</button>
Loading
Loading
@@ -128,11 +127,10 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
type="button"
>
<svg
aria-hidden="true"
class="s16 ic-duplicate"
class="gl-icon s16"
>
<use
xlink:href="#duplicate"
href="#copy-to-clipboard"
/>
</svg>
</button>
Loading
Loading
@@ -158,11 +156,10 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
type="button"
>
<svg
aria-hidden="true"
class="s16 ic-duplicate"
class="gl-icon s16"
>
<use
xlink:href="#duplicate"
href="#copy-to-clipboard"
/>
</svg>
</button>
Loading
Loading
import SnippetApp from '~/snippets/components/app.vue';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
import SnippetTitle from '~/snippets/components/snippet_title.vue';
import SnippetBlob from '~/snippets/components/snippet_blob_view.vue';
import { GlLoadingIcon } from '@gitlab/ui';
 
import { shallowMount } from '@vue/test-utils';
Loading
Loading
@@ -35,8 +37,10 @@ describe('Snippet view app', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
 
it('renders SnippetHeader component after the query is finished', () => {
it('renders all components after the query is finished', () => {
createComponent();
expect(wrapper.find(SnippetHeader).exists()).toBe(true);
expect(wrapper.find(SnippetTitle).exists()).toBe(true);
expect(wrapper.find(SnippetBlob).exists()).toBe(true);
});
});
import { shallowMount } from '@vue/test-utils';
import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import {
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
describe('Blob Embeddable', () => {
let wrapper;
const snippet = {
id: 'gid://foo.bar/snippet',
webUrl: 'https://foo.bar',
visibilityLevel: SNIPPET_VISIBILITY_PUBLIC,
};
function createComponent(props = {}) {
wrapper = shallowMount(SnippetBlobView, {
propsData: {
snippet: {
...snippet,
...props,
},
},
});
}
afterEach(() => {
wrapper.destroy();
});
it('renders blob-embeddable component', () => {
createComponent();
expect(wrapper.find(BlobEmbeddable).exists()).toBe(true);
});
it('does not render blob-embeddable for internal snippet', () => {
createComponent({
visibilityLevel: SNIPPET_VISIBILITY_INTERNAL,
});
expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
createComponent({
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
});
expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
createComponent({
visibilityLevel: 'foo',
});
expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
});
});
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import { GlButton, GlIcon } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
 
describe('clipboard button', () => {
let wrapper;
Loading
Loading
@@ -29,7 +28,7 @@ describe('clipboard button', () => {
it('renders a button for clipboard', () => {
expect(wrapper.find(GlButton).exists()).toBe(true);
expect(wrapper.attributes('data-clipboard-text')).toBe('copy me');
expect(wrapper.find(Icon).props('name')).toBe('duplicate');
expect(wrapper.find(GlIcon).props('name')).toBe('copy-to-clipboard');
});
 
it('should have a tooltip with default values', () => {
Loading
Loading
Loading
Loading
@@ -54,97 +54,6 @@ describe('date time picker lib', () => {
});
});
 
describe('getTimeWindow', () => {
[
{
args: [
{
start: '2019-10-01T18:27:47.000Z',
end: '2019-10-01T21:27:47.000Z',
},
dateTimePickerLib.defaultTimeWindows,
],
expected: 'threeHours',
},
{
args: [
{
start: '2019-10-01T28:27:47.000Z',
end: '2019-10-01T21:27:47.000Z',
},
dateTimePickerLib.defaultTimeWindows,
],
expected: null,
},
{
args: [
{
start: '',
end: '',
},
dateTimePickerLib.defaultTimeWindows,
],
expected: null,
},
{
args: [
{
start: null,
end: null,
},
dateTimePickerLib.defaultTimeWindows,
],
expected: null,
},
{
args: [{}, dateTimePickerLib.defaultTimeWindows],
expected: null,
},
].forEach(({ args, expected }) => {
it(`returns "${expected}" with args=${JSON.stringify(args)}`, () => {
expect(dateTimePickerLib.getTimeWindowKey(...args)).toEqual(expected);
});
});
});
describe('getTimeRange', () => {
function secondsBetween({ start, end }) {
return (new Date(end) - new Date(start)) / 1000;
}
function minutesBetween(timeRange) {
return secondsBetween(timeRange) / 60;
}
function hoursBetween(timeRange) {
return minutesBetween(timeRange) / 60;
}
it('defaults to an 8 hour (28800s) difference', () => {
const params = dateTimePickerLib.getTimeRange();
expect(hoursBetween(params)).toEqual(8);
});
it('accepts time window as an argument', () => {
const params = dateTimePickerLib.getTimeRange('thirtyMinutes');
expect(minutesBetween(params)).toEqual(30);
});
it('returns a value for every defined time window', () => {
const nonDefaultWindows = Object.entries(dateTimePickerLib.defaultTimeWindows).filter(
([, timeWindow]) => !timeWindow.default,
);
nonDefaultWindows.forEach(timeWindow => {
const params = dateTimePickerLib.getTimeRange(timeWindow[0]);
// Ensure we're not returning the default
expect(hoursBetween(params)).not.toEqual(8);
});
});
});
describe('stringToISODate', () => {
['', 'null', undefined, 'abc'].forEach(input => {
it(`throws error for invalid input like ${input}`, done => {
Loading
Loading
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