Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
78f7d2e726
commit
652d8b33ee
|
@ -82,7 +82,7 @@ In this rollout issue, ensure the scoped `experiment::` label is kept accurate.
|
|||
|
||||
## Roll Out Steps
|
||||
|
||||
- [ ] Confirm that QA tests pass with the feature flag enabled (if you're unsure how, contact the relevant [stable counterpart in the Quality department](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors))
|
||||
- [ ] [Confirm that end-to-end tests pass with the feature flag enabled](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/feature_flags.html#confirming-that-end-to-end-tests-pass-with-a-feature-flag-enabled). If there are failing tests, contact the relevant [stable counterpart in the Quality department](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors) to collaborate in updating the tests or confirming that the failing tests are not caused by the changes behind the enabled feature flag.
|
||||
- [ ] Enable on staging (`/chatops run feature set <experiment-key> true --staging`)
|
||||
- [ ] Test on staging
|
||||
- [ ] Ensure that documentation has been updated
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
<script>
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { GlFormInput } from '@gitlab/ui';
|
||||
import {
|
||||
DurationParseError,
|
||||
outputChronicDuration,
|
||||
parseChronicDuration,
|
||||
} from '~/chronic_duration';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlFormInput,
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
integerRequired: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
numberData: this.value,
|
||||
humanReadableData: this.convertDuration(this.value),
|
||||
isValueValid: this.value === null ? null : true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
numberValue: {
|
||||
get() {
|
||||
return this.numberData;
|
||||
},
|
||||
set(value) {
|
||||
if (this.numberData !== value) {
|
||||
this.numberData = value;
|
||||
this.humanReadableData = this.convertDuration(value);
|
||||
this.isValueValid = value === null ? null : true;
|
||||
}
|
||||
this.emitEvents();
|
||||
},
|
||||
},
|
||||
humanReadableValue: {
|
||||
get() {
|
||||
return this.humanReadableData;
|
||||
},
|
||||
set(value) {
|
||||
this.humanReadableData = value;
|
||||
try {
|
||||
if (value === '') {
|
||||
this.numberData = null;
|
||||
this.isValueValid = null;
|
||||
} else {
|
||||
this.numberData = parseChronicDuration(value, {
|
||||
keepZero: true,
|
||||
raiseExceptions: true,
|
||||
});
|
||||
this.isValueValid = true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof DurationParseError) {
|
||||
this.isValueValid = false;
|
||||
} else {
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
}
|
||||
this.emitEvents(true);
|
||||
},
|
||||
},
|
||||
isValidDecimal() {
|
||||
return !this.integerRequired || this.numberData === null || Number.isInteger(this.numberData);
|
||||
},
|
||||
feedback() {
|
||||
if (this.isValueValid === false) {
|
||||
return this.$options.i18n.INVALID_INPUT_FEEDBACK;
|
||||
}
|
||||
if (!this.isValidDecimal) {
|
||||
return this.$options.i18n.INVALID_DECIMAL_FEEDBACK;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
INVALID_INPUT_FEEDBACK: __('Please enter a valid time interval'),
|
||||
INVALID_DECIMAL_FEEDBACK: __('An integer value is required for seconds'),
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.numberValue = this.value;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.emitEvents();
|
||||
},
|
||||
methods: {
|
||||
convertDuration(value) {
|
||||
return value === null ? '' : outputChronicDuration(value);
|
||||
},
|
||||
emitEvents(emitChange = false) {
|
||||
if (emitChange && this.isValueValid !== false && this.isValidDecimal) {
|
||||
this.$emit('change', this.numberData);
|
||||
}
|
||||
const { feedback } = this;
|
||||
this.$refs.text.$el.setCustomValidity(feedback);
|
||||
this.$refs.hidden.setCustomValidity(feedback);
|
||||
this.$emit('valid', {
|
||||
valid: this.isValueValid && this.isValidDecimal,
|
||||
feedback,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<gl-form-input ref="text" v-bind="$attrs" v-model="humanReadableValue" />
|
||||
<input ref="hidden" type="hidden" :name="name" :value="numberValue" />
|
||||
</div>
|
||||
</template>
|
|
@ -16,94 +16,81 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
> - Overview table [added](https://gitlab.com/gitlab-org/gitlab/-/issues/335638) in GitLab 14.3.
|
||||
> - Adoption over time chart [added](https://gitlab.com/gitlab-org/gitlab/-/issues/337561) in GitLab 14.4.
|
||||
|
||||
Prerequisites:
|
||||
DevOps Adoption shows you how groups in your organization adopt and use the most essential features of GitLab.
|
||||
|
||||
- You must have at least the [Reporter role](../../permissions.md) for the group.
|
||||
You can use Group DevOps Adoption to:
|
||||
|
||||
To access Group DevOps Adoption, go to your group and select **Analytics > DevOps Adoption**.
|
||||
|
||||
Group DevOps Adoption shows you how individual groups and subgroups within your organization use the following features:
|
||||
|
||||
- Dev
|
||||
- Approvals
|
||||
- Code owners
|
||||
- Issues
|
||||
- Merge requests
|
||||
- Sec
|
||||
- DAST
|
||||
- Dependency Scanning
|
||||
- Fuzz Testing
|
||||
- SAST
|
||||
- Ops
|
||||
- Deployments
|
||||
- Pipelines
|
||||
- Runners
|
||||
|
||||
When managing groups in the UI, you can add or remove your subgroups with the **Add or remove subgroups**
|
||||
button, in the top right hand section of your Groups pages.
|
||||
|
||||
With DevOps Adoption you can:
|
||||
|
||||
- Verify whether you are getting the return on investment that you expected from GitLab.
|
||||
- Identify specific subgroups that are lagging in their adoption of GitLab, so you can help them along in their DevOps journey.
|
||||
- Find the subgroups that have adopted certain features, and can provide guidance to other subgroups on how to use those features.
|
||||
- Identify specific subgroups that are lagging in their adoption of GitLab features, so you can guide them on
|
||||
their DevOps journey.
|
||||
- Find subgroups that have adopted certain features, and provide guidance to other subgroups on
|
||||
how to use those features.
|
||||
- Verify if you are getting the return on investment that you expected from GitLab.
|
||||
|
||||
![DevOps Adoption](img/group_devops_adoption_v14_2.png)
|
||||
|
||||
Feature adoption is based on usage in the previous calendar month. Data is updated on the first day
|
||||
of each month. If the monthly update fails, it tries again daily until successful.
|
||||
## View DevOps Adoption
|
||||
|
||||
## Enable data processing
|
||||
Prerequisite:
|
||||
|
||||
Group DevOps Adoption relies on data that has been gathered by a weekly data processing task.
|
||||
This task is disabled by default.
|
||||
- You must have at least the [Reporter role](../../permissions.md) for the group.
|
||||
|
||||
To begin using Group DevOps Adoption, access the feature for the first time. GitLab automatically
|
||||
enables the data processing for that group. The group data doesn't appear immediately, because
|
||||
GitLab requires around a minute to process it.
|
||||
To view DevOps Adoption:
|
||||
|
||||
## What is displayed
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > DevOps adoption**
|
||||
|
||||
DevOps Adoption displays feature adoption data for the given group
|
||||
and any added subgroups for the current calendar month.
|
||||
Each group appears as a separate row in the table.
|
||||
For each row, a feature is considered "adopted" if it has been used in a project in the given group
|
||||
during the time period (including projects in any subgroups of the given group).
|
||||
## DevOps Adoption categories
|
||||
|
||||
## Adoption over time
|
||||
DevOps Adoption shows feature adoption for development, security, and operations.
|
||||
|
||||
The **Adoption over time** chart in the **Overview** tab displays DevOps Adoption over time. The chart displays the total number of adopted features from the previous twelve months,
|
||||
from when you enabled DevOps Adoption for the group.
|
||||
| Category | Feature |
|
||||
| --- | --- |
|
||||
| Development | Approvals<br>Code owners<br>Issues<br>Merge requests |
|
||||
| Security | DAST<br>Dependency Scanning<br>Fuzz Testing<br>SAST |
|
||||
| Operations | Deployments<br>Pipelines<br>Runners |
|
||||
|
||||
The tooltip displays information about the features tracked for individual months.
|
||||
## Feature adoption
|
||||
|
||||
## When is a feature considered adopted
|
||||
DevOps Adoption shows feature adoption data for groups and subgroups for the previous calendar month.
|
||||
|
||||
A feature is considered "adopted" if it has been used anywhere in the group in the specified time.
|
||||
For example, if an issue was created in one project in a group, the group is considered to have
|
||||
"adopted" issues in that time.
|
||||
A feature shows as **adopted** when a group has used the feature in a project during the time period.
|
||||
This includes projects in any subgroups of the group. For example, if an issue was created in a project in a group, the group has adopted issues in that time.
|
||||
|
||||
## No penalties for common adoption patterns
|
||||
### Exceptions to feature adoption data
|
||||
|
||||
DevOps Adoption is designed not to penalize for any circumstances or practices that are common in DevOps.
|
||||
Following this guideline, GitLab doesn't penalize for:
|
||||
When GitLab measures DevOps Adoption, some common DevOps information is not included:
|
||||
|
||||
1. Having dormant projects. It's common for groups to have a mix of active and dormant projects,
|
||||
so we should not consider adoption to be low if there are relatively many dormant projects.
|
||||
This means we should not measure adoption by how many projects in the group have used a feature,
|
||||
only by whether a feature was used anywhere in the group.
|
||||
1. GitLab adding new features over time. It's common for group feature usage to be consistent
|
||||
over time, so we should not consider adoption to have decreased if GitLab adds features.
|
||||
This means we should not measure adoption by percentages, only total counts.
|
||||
- Dormant projects. It doesn't matter how many projects in the group use a feature. Even if you have many dormant projects, it doesn't lower the adoption.
|
||||
- New GitLab features. Adoption is the total number of features adopted, not the percent of features.
|
||||
|
||||
## Add a subgroup
|
||||
## When DevOps Adoption data is gathered
|
||||
|
||||
DevOps Adoption can also display data for subgroups within the given group,
|
||||
to show you differences in adoption across the group.
|
||||
To add subgroups to your Group DevOps Adoption report:
|
||||
A weekly task processes data for DevOps Adoption. This task is disabled until you access
|
||||
DevOps Adoption for a group for the first time.
|
||||
|
||||
The data processing task updates the data on the first day of each month. If the monthly update
|
||||
fails, the task tries daily until it succeeds.
|
||||
|
||||
DevOps Adoption data may take up to a minute to appear while GitLab processes the group's data.
|
||||
|
||||
## View feature adoption over time
|
||||
|
||||
The **Adoption over time** chart shows the total number of adopted features from the previous
|
||||
twelve months. The chart only shows data from when you enabled DevOps Adoption for the group.
|
||||
|
||||
To view feature adoption over time:
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > DevOps adoption**.
|
||||
1. Select the **Overview** tab.
|
||||
|
||||
Tooltips display information about the features tracked for individual months.
|
||||
|
||||
## Add or remove a subgroup
|
||||
|
||||
To add or remove a subgroup from the DevOps Adoption report:
|
||||
|
||||
1. Select **Add or remove subgroups**.
|
||||
1. Select the subgroups you want to add and select **Save changes**.
|
||||
1. Select the subgroup you want to add or remove and select **Save changes**.
|
||||
|
||||
The subgroup data might not appear immediately, because GitLab requires around a minute to collect
|
||||
the data.
|
||||
It may take up to a minute for subgroup data to appear while GitLab collects the data.
|
||||
|
|
|
@ -3917,6 +3917,9 @@ msgstr ""
|
|||
msgid "An example showing how to use Jsonnet with GitLab dynamic child pipelines"
|
||||
msgstr ""
|
||||
|
||||
msgid "An integer value is required for seconds"
|
||||
msgstr ""
|
||||
|
||||
msgid "An issue already exists"
|
||||
msgstr ""
|
||||
|
||||
|
@ -25974,6 +25977,9 @@ msgstr ""
|
|||
msgid "Please enter a valid number"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please enter a valid time interval"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please enter or upload a valid license."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "1.221.0",
|
||||
"@gitlab/tributejs": "1.0.0",
|
||||
"@gitlab/ui": "32.38.0",
|
||||
"@gitlab/ui": "32.39.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "6.1.4-1",
|
||||
"@rails/ujs": "6.1.4-1",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Create' do
|
||||
RSpec.describe 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/346149', type: :stale } do
|
||||
context 'Content Editor' do
|
||||
let(:initial_wiki) { Resource::Wiki::ProjectPage.fabricate_via_api! }
|
||||
let(:page_title) { 'Content Editor Page' }
|
||||
|
|
|
@ -0,0 +1,390 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue';
|
||||
|
||||
const MOCK_VALUE = 2 * 3600 + 20 * 60;
|
||||
|
||||
describe('vue_shared/components/chronic_duration_input', () => {
|
||||
let wrapper;
|
||||
let textElement;
|
||||
let hiddenElement;
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
textElement = null;
|
||||
hiddenElement = null;
|
||||
});
|
||||
|
||||
const findComponents = () => {
|
||||
textElement = wrapper.find('input[type=text]').element;
|
||||
hiddenElement = wrapper.find('input[type=hidden]').element;
|
||||
};
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
if (wrapper) {
|
||||
throw new Error('There should only be one wrapper created per test');
|
||||
}
|
||||
|
||||
wrapper = mount(ChronicDurationInput, { propsData: props });
|
||||
findComponents();
|
||||
};
|
||||
|
||||
describe('value', () => {
|
||||
it('has human-readable output with value', () => {
|
||||
createComponent({ value: MOCK_VALUE });
|
||||
|
||||
expect(textElement.value).toBe('2 hrs 20 mins');
|
||||
expect(hiddenElement.value).toBe(MOCK_VALUE.toString());
|
||||
});
|
||||
|
||||
it('has empty output with no value', () => {
|
||||
createComponent({ value: null });
|
||||
|
||||
expect(textElement.value).toBe('');
|
||||
expect(hiddenElement.value).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('change', () => {
|
||||
const createAndDispatch = async (initialValue, humanReadableInput) => {
|
||||
createComponent({ value: initialValue });
|
||||
await wrapper.vm.$nextTick();
|
||||
textElement.value = humanReadableInput;
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
};
|
||||
|
||||
describe('when starting with no value and receiving human-readable input', () => {
|
||||
beforeEach(() => {
|
||||
createAndDispatch(null, '2hr20min');
|
||||
});
|
||||
|
||||
it('updates hidden field', () => {
|
||||
expect(textElement.value).toBe('2hr20min');
|
||||
expect(hiddenElement.value).toBe(MOCK_VALUE.toString());
|
||||
});
|
||||
|
||||
it('emits change event', () => {
|
||||
expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when starting with a value and receiving empty input', () => {
|
||||
beforeEach(() => {
|
||||
createAndDispatch(MOCK_VALUE, '');
|
||||
});
|
||||
|
||||
it('updates hidden field', () => {
|
||||
expect(textElement.value).toBe('');
|
||||
expect(hiddenElement.value).toBe('');
|
||||
});
|
||||
|
||||
it('emits change event', () => {
|
||||
expect(wrapper.emitted('change')).toEqual([[null]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when starting with a value and receiving invalid input', () => {
|
||||
beforeEach(() => {
|
||||
createAndDispatch(MOCK_VALUE, 'gobbledygook');
|
||||
});
|
||||
|
||||
it('does not update hidden field', () => {
|
||||
expect(textElement.value).toBe('gobbledygook');
|
||||
expect(hiddenElement.value).toBe(MOCK_VALUE.toString());
|
||||
});
|
||||
|
||||
it('does not emit change event', () => {
|
||||
expect(wrapper.emitted('change')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid', () => {
|
||||
describe('initial value', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ value: MOCK_VALUE });
|
||||
});
|
||||
|
||||
it('emits valid with initial value', () => {
|
||||
expect(wrapper.emitted('valid')).toEqual([[{ valid: true, feedback: '' }]]);
|
||||
expect(textElement.validity.valid).toBe(true);
|
||||
expect(textElement.validity.customError).toBe(false);
|
||||
expect(textElement.validationMessage).toBe('');
|
||||
expect(hiddenElement.validity.valid).toBe(true);
|
||||
expect(hiddenElement.validity.customError).toBe(false);
|
||||
expect(hiddenElement.validationMessage).toBe('');
|
||||
});
|
||||
|
||||
it('emits valid with user input', async () => {
|
||||
textElement.value = '1m10s';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('valid')).toEqual([
|
||||
[{ valid: true, feedback: '' }],
|
||||
[{ valid: true, feedback: '' }],
|
||||
]);
|
||||
expect(textElement.validity.valid).toBe(true);
|
||||
expect(textElement.validity.customError).toBe(false);
|
||||
expect(textElement.validationMessage).toBe('');
|
||||
expect(hiddenElement.validity.valid).toBe(true);
|
||||
expect(hiddenElement.validity.customError).toBe(false);
|
||||
expect(hiddenElement.validationMessage).toBe('');
|
||||
|
||||
textElement.value = '';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('valid')).toEqual([
|
||||
[{ valid: true, feedback: '' }],
|
||||
[{ valid: true, feedback: '' }],
|
||||
[{ valid: null, feedback: '' }],
|
||||
]);
|
||||
expect(textElement.validity.valid).toBe(true);
|
||||
expect(textElement.validity.customError).toBe(false);
|
||||
expect(textElement.validationMessage).toBe('');
|
||||
expect(hiddenElement.validity.valid).toBe(true);
|
||||
expect(hiddenElement.validity.customError).toBe(false);
|
||||
expect(hiddenElement.validationMessage).toBe('');
|
||||
});
|
||||
|
||||
it('emits invalid with user input', async () => {
|
||||
textElement.value = 'gobbledygook';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('valid')).toEqual([
|
||||
[{ valid: true, feedback: '' }],
|
||||
[{ valid: false, feedback: ChronicDurationInput.i18n.INVALID_INPUT_FEEDBACK }],
|
||||
]);
|
||||
expect(textElement.validity.valid).toBe(false);
|
||||
expect(textElement.validity.customError).toBe(true);
|
||||
expect(textElement.validationMessage).toBe(
|
||||
ChronicDurationInput.i18n.INVALID_INPUT_FEEDBACK,
|
||||
);
|
||||
expect(hiddenElement.validity.valid).toBe(false);
|
||||
expect(hiddenElement.validity.customError).toBe(true);
|
||||
// Hidden elements do not have validationMessage
|
||||
expect(hiddenElement.validationMessage).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('no initial value', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ value: null });
|
||||
});
|
||||
|
||||
it('emits valid with no initial value', () => {
|
||||
expect(wrapper.emitted('valid')).toEqual([[{ valid: null, feedback: '' }]]);
|
||||
expect(textElement.validity.valid).toBe(true);
|
||||
expect(textElement.validity.customError).toBe(false);
|
||||
expect(textElement.validationMessage).toBe('');
|
||||
expect(hiddenElement.validity.valid).toBe(true);
|
||||
expect(hiddenElement.validity.customError).toBe(false);
|
||||
expect(hiddenElement.validationMessage).toBe('');
|
||||
});
|
||||
|
||||
it('emits valid with updated value', async () => {
|
||||
wrapper.setProps({ value: MOCK_VALUE });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('valid')).toEqual([
|
||||
[{ valid: null, feedback: '' }],
|
||||
[{ valid: true, feedback: '' }],
|
||||
]);
|
||||
expect(textElement.validity.valid).toBe(true);
|
||||
expect(textElement.validity.customError).toBe(false);
|
||||
expect(textElement.validationMessage).toBe('');
|
||||
expect(hiddenElement.validity.valid).toBe(true);
|
||||
expect(hiddenElement.validity.customError).toBe(false);
|
||||
expect(hiddenElement.validationMessage).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decimal input', () => {
|
||||
describe('when integerRequired is false', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ value: null, integerRequired: false });
|
||||
});
|
||||
|
||||
it('emits valid when input is integer', async () => {
|
||||
textElement.value = '2hr20min';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
|
||||
expect(wrapper.emitted('valid')).toEqual([
|
||||
[{ valid: null, feedback: '' }],
|
||||
[{ valid: true, feedback: '' }],
|
||||
]);
|
||||
expect(textElement.validity.valid).toBe(true);
|
||||
expect(textElement.validity.customError).toBe(false);
|
||||
expect(textElement.validationMessage).toBe('');
|
||||
expect(hiddenElement.validity.valid).toBe(true);
|
||||
expect(hiddenElement.validity.customError).toBe(false);
|
||||
expect(hiddenElement.validationMessage).toBe('');
|
||||
});
|
||||
|
||||
it('emits valid when input is decimal', async () => {
|
||||
textElement.value = '1.5s';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('change')).toEqual([[1.5]]);
|
||||
expect(wrapper.emitted('valid')).toEqual([
|
||||
[{ valid: null, feedback: '' }],
|
||||
[{ valid: true, feedback: '' }],
|
||||
]);
|
||||
expect(textElement.validity.valid).toBe(true);
|
||||
expect(textElement.validity.customError).toBe(false);
|
||||
expect(textElement.validationMessage).toBe('');
|
||||
expect(hiddenElement.validity.valid).toBe(true);
|
||||
expect(hiddenElement.validity.customError).toBe(false);
|
||||
expect(hiddenElement.validationMessage).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when integerRequired is unspecified', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ value: null });
|
||||
});
|
||||
|
||||
it('emits valid when input is integer', async () => {
|
||||
textElement.value = '2hr20min';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
|
||||
expect(wrapper.emitted('valid')).toEqual([
|
||||
[{ valid: null, feedback: '' }],
|
||||
[{ valid: true, feedback: '' }],
|
||||
]);
|
||||
expect(textElement.validity.valid).toBe(true);
|
||||
expect(textElement.validity.customError).toBe(false);
|
||||
expect(textElement.validationMessage).toBe('');
|
||||
expect(hiddenElement.validity.valid).toBe(true);
|
||||
expect(hiddenElement.validity.customError).toBe(false);
|
||||
expect(hiddenElement.validationMessage).toBe('');
|
||||
});
|
||||
|
||||
it('emits invalid when input is decimal', async () => {
|
||||
textElement.value = '1.5s';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('change')).toBeUndefined();
|
||||
expect(wrapper.emitted('valid')).toEqual([
|
||||
[{ valid: null, feedback: '' }],
|
||||
[
|
||||
{
|
||||
valid: false,
|
||||
feedback: ChronicDurationInput.i18n.INVALID_DECIMAL_FEEDBACK,
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(textElement.validity.valid).toBe(false);
|
||||
expect(textElement.validity.customError).toBe(true);
|
||||
expect(textElement.validationMessage).toBe(
|
||||
ChronicDurationInput.i18n.INVALID_DECIMAL_FEEDBACK,
|
||||
);
|
||||
expect(hiddenElement.validity.valid).toBe(false);
|
||||
expect(hiddenElement.validity.customError).toBe(true);
|
||||
// Hidden elements do not have validationMessage
|
||||
expect(hiddenElement.validationMessage).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('v-model', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount({
|
||||
data() {
|
||||
return { value: 1 * 60 + 10 };
|
||||
},
|
||||
components: { ChronicDurationInput },
|
||||
template: '<div><chronic-duration-input v-model="value"/></div>',
|
||||
});
|
||||
findComponents();
|
||||
});
|
||||
|
||||
describe('value', () => {
|
||||
it('passes initial prop via v-model', () => {
|
||||
expect(textElement.value).toBe('1 min 10 secs');
|
||||
expect(hiddenElement.value).toBe((1 * 60 + 10).toString());
|
||||
});
|
||||
|
||||
it('passes updated prop via v-model', async () => {
|
||||
wrapper.setData({ value: MOCK_VALUE });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(textElement.value).toBe('2 hrs 20 mins');
|
||||
expect(hiddenElement.value).toBe(MOCK_VALUE.toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('change', () => {
|
||||
it('passes user input to parent via v-model', async () => {
|
||||
textElement.value = '2hr20min';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent(ChronicDurationInput).props('value')).toBe(MOCK_VALUE);
|
||||
expect(textElement.value).toBe('2hr20min');
|
||||
expect(hiddenElement.value).toBe(MOCK_VALUE.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('name', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ name: 'myInput' });
|
||||
});
|
||||
|
||||
it('sets name of hidden field', () => {
|
||||
expect(hiddenElement.name).toBe('myInput');
|
||||
});
|
||||
|
||||
it('does not set name of text field', () => {
|
||||
expect(textElement.name).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('form submission', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount({
|
||||
template: `<form data-testid="myForm"><chronic-duration-input name="myInput" :value="${MOCK_VALUE}"/></form>`,
|
||||
components: {
|
||||
ChronicDurationInput,
|
||||
},
|
||||
});
|
||||
findComponents();
|
||||
});
|
||||
|
||||
it('creates form data with initial value', () => {
|
||||
const formData = new FormData(wrapper.find('[data-testid=myForm]').element);
|
||||
const iter = formData.entries();
|
||||
|
||||
expect(iter.next()).toEqual({
|
||||
value: ['myInput', MOCK_VALUE.toString()],
|
||||
done: false,
|
||||
});
|
||||
expect(iter.next()).toEqual({ value: undefined, done: true });
|
||||
});
|
||||
|
||||
it('creates form data with user-specified value', async () => {
|
||||
textElement.value = '1m10s';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const formData = new FormData(wrapper.find('[data-testid=myForm]').element);
|
||||
const iter = formData.entries();
|
||||
|
||||
expect(iter.next()).toEqual({
|
||||
value: ['myInput', (1 * 60 + 10).toString()],
|
||||
done: false,
|
||||
});
|
||||
expect(iter.next()).toEqual({ value: undefined, done: true });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -924,10 +924,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
|
||||
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
|
||||
|
||||
"@gitlab/ui@32.38.0":
|
||||
version "32.38.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.38.0.tgz#580bda8daacdf23c1f6a75d77f6df44b1dbe1726"
|
||||
integrity sha512-BS0+4JicfuiCbaWTTok0dQUzUCI8m8t5T7//DQUQqqwCZLYeJlb1AxMatd6IjwcdE0m+AhST3iOZi2x+hDrkbQ==
|
||||
"@gitlab/ui@32.39.0":
|
||||
version "32.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.39.0.tgz#81dd3fd730721e828f62f6aff0a3f41afc3ac309"
|
||||
integrity sha512-SgbmTkjpMFoTGREkJM7D7/VnzQjdmAikF45pqQJVJWZtGSLAadIbuZQBFUYwa9SFC6BhXjeoSmyRxBr1sdB19A==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
bootstrap-vue "2.20.1"
|
||||
|
|
Loading…
Reference in New Issue