@gitlab/ui 60.2.0 → 61.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/dist/components/base/banner/banner.js +11 -1
- package/dist/components/base/filtered_search/filtered_search.js +1 -4
- package/dist/components/base/filtered_search/filtered_search_suggestion.js +1 -1
- package/dist/components/base/filtered_search/filtered_search_suggestion_list.js +30 -16
- package/dist/components/base/filtered_search/filtered_search_term.js +4 -6
- package/dist/components/base/filtered_search/filtered_search_token.js +16 -17
- package/dist/components/base/filtered_search/filtered_search_token_segment.js +23 -14
- package/dist/components/base/filtered_search/filtered_search_utils.js +78 -1
- package/dist/utils/number_utils.js +22 -1
- package/package.json +1 -1
- package/src/components/base/banner/banner.vue +11 -9
- package/src/components/base/filtered_search/__snapshots__/filtered_search_term.spec.js.snap +2 -0
- package/src/components/base/filtered_search/filtered_search.md +5 -3
- package/src/components/base/filtered_search/filtered_search.stories.js +8 -5
- package/src/components/base/filtered_search/filtered_search.vue +1 -11
- package/src/components/base/filtered_search/filtered_search_suggestion.md +8 -2
- package/src/components/base/filtered_search/filtered_search_suggestion.vue +1 -0
- package/src/components/base/filtered_search/filtered_search_suggestion_list.spec.js +61 -64
- package/src/components/base/filtered_search/filtered_search_suggestion_list.vue +39 -20
- package/src/components/base/filtered_search/filtered_search_term.spec.js +11 -28
- package/src/components/base/filtered_search/filtered_search_term.vue +6 -17
- package/src/components/base/filtered_search/filtered_search_token.spec.js +3 -22
- package/src/components/base/filtered_search/filtered_search_token.vue +8 -16
- package/src/components/base/filtered_search/filtered_search_token_segment.spec.js +18 -1
- package/src/components/base/filtered_search/filtered_search_token_segment.stories.js +9 -0
- package/src/components/base/filtered_search/filtered_search_token_segment.vue +35 -12
- package/src/components/base/filtered_search/filtered_search_utils.js +69 -0
- package/src/components/base/filtered_search/filtered_search_utils.spec.js +32 -1
- package/src/utils/number_utils.js +21 -0
- package/src/utils/number_utils.spec.js +42 -0
|
@@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
|
|
3
3
|
import { COMMA } from '../../../utils/constants';
|
|
4
4
|
import GlToken from '../token/token.vue';
|
|
5
5
|
import GlFilteredSearchTokenSegment from './filtered_search_token_segment.vue';
|
|
6
|
-
import { createTerm } from './filtered_search_utils';
|
|
6
|
+
import { createTerm, tokenToOption } from './filtered_search_utils';
|
|
7
7
|
|
|
8
8
|
const SEGMENT_TITLE = 'TYPE';
|
|
9
9
|
const SEGMENT_OPERATOR = 'OPERATOR';
|
|
@@ -11,7 +11,7 @@ const SEGMENT_DATA = 'DATA';
|
|
|
11
11
|
const TOKEN_CLOSE_SELECTOR = '.gl-token-close';
|
|
12
12
|
|
|
13
13
|
const DEFAULT_OPERATORS = [
|
|
14
|
-
{ value: '=', description: 'is', default:
|
|
14
|
+
{ value: '=', description: 'is', default: true },
|
|
15
15
|
{ value: '!=', description: 'is not' },
|
|
16
16
|
];
|
|
17
17
|
|
|
@@ -98,10 +98,9 @@ export default {
|
|
|
98
98
|
},
|
|
99
99
|
|
|
100
100
|
availableTokensWithSelf() {
|
|
101
|
-
return [this.config, ...this.availableTokens.filter((
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}));
|
|
101
|
+
return [this.config, ...this.availableTokens.filter((token) => token !== this.config)].map(
|
|
102
|
+
tokenToOption
|
|
103
|
+
);
|
|
105
104
|
},
|
|
106
105
|
|
|
107
106
|
operatorDescription() {
|
|
@@ -212,8 +211,8 @@ export default {
|
|
|
212
211
|
}
|
|
213
212
|
},
|
|
214
213
|
|
|
215
|
-
replaceToken(
|
|
216
|
-
const newTokenConfig = this.availableTokens.find((
|
|
214
|
+
replaceToken(newType) {
|
|
215
|
+
const newTokenConfig = this.availableTokens.find(({ type }) => type === newType);
|
|
217
216
|
|
|
218
217
|
if (newTokenConfig === this.config) {
|
|
219
218
|
this.$nextTick(() => {
|
|
@@ -303,10 +302,6 @@ export default {
|
|
|
303
302
|
this.$emit('destroy');
|
|
304
303
|
}
|
|
305
304
|
},
|
|
306
|
-
|
|
307
|
-
hasTitleOptionSlot() {
|
|
308
|
-
return Boolean(this.$scopedSlots['title-option']);
|
|
309
|
-
},
|
|
310
305
|
},
|
|
311
306
|
};
|
|
312
307
|
</script>
|
|
@@ -350,9 +345,6 @@ export default {
|
|
|
350
345
|
{{ inputValue }}
|
|
351
346
|
</gl-token>
|
|
352
347
|
</template>
|
|
353
|
-
<template v-if="hasTitleOptionSlot()" #option>
|
|
354
|
-
<slot name="title-option"></slot>
|
|
355
|
-
</template>
|
|
356
348
|
</gl-filtered-search-token-segment>
|
|
357
349
|
|
|
358
350
|
<gl-filtered-search-token-segment
|
|
@@ -361,6 +353,7 @@ export default {
|
|
|
361
353
|
:active="isSegmentActive($options.segments.SEGMENT_OPERATOR)"
|
|
362
354
|
:cursor-position="intendedCursorPosition"
|
|
363
355
|
:options="operators"
|
|
356
|
+
option-text-field="value"
|
|
364
357
|
:custom-input-keydown-handler="handleOperatorKeydown"
|
|
365
358
|
:view-only="viewOnly"
|
|
366
359
|
@activate="activateSegment($options.segments.SEGMENT_OPERATOR)"
|
|
@@ -412,7 +405,6 @@ export default {
|
|
|
412
405
|
:multi-select="config.multiSelect"
|
|
413
406
|
:options="config.options"
|
|
414
407
|
:view-only="viewOnly"
|
|
415
|
-
option-text-field="title"
|
|
416
408
|
@activate="activateDataSegment"
|
|
417
409
|
@backspace="activateSegment($options.segments.SEGMENT_OPERATOR)"
|
|
418
410
|
@complete="handleComplete"
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { shallowMount } from '@vue/test-utils';
|
|
2
2
|
import GlFilteredSearchTokenSegment from './filtered_search_token_segment.vue';
|
|
3
3
|
|
|
4
|
-
const OPTIONS = [
|
|
4
|
+
const OPTIONS = [
|
|
5
|
+
{ value: '=', title: 'is' },
|
|
6
|
+
{ value: '!=', title: 'is not' },
|
|
7
|
+
];
|
|
5
8
|
|
|
6
9
|
describe('Filtered search token segment', () => {
|
|
7
10
|
let wrapper;
|
|
@@ -330,5 +333,19 @@ describe('Filtered search token segment', () => {
|
|
|
330
333
|
createWrappedComponent({ value: 'test', active: true, viewOnly: false });
|
|
331
334
|
expect(wrapper.find('input').attributes('readonly')).toBeUndefined();
|
|
332
335
|
});
|
|
336
|
+
|
|
337
|
+
it.each`
|
|
338
|
+
context | isTerm | eventPayloads
|
|
339
|
+
${'does'} | ${false} | ${[['!=']]}
|
|
340
|
+
${'does not'} | ${true} | ${undefined}
|
|
341
|
+
`(
|
|
342
|
+
'$context revert to fallback value on deactivation when no-fallback is $isTerm',
|
|
343
|
+
async ({ isTerm, eventPayloads }) => {
|
|
344
|
+
createComponent({ value: '!=', active: true, options: OPTIONS, isTerm });
|
|
345
|
+
|
|
346
|
+
await wrapper.setProps({ value: 'foo', active: false });
|
|
347
|
+
expect(wrapper.emitted('input')).toEqual(eventPayloads);
|
|
348
|
+
}
|
|
349
|
+
);
|
|
333
350
|
});
|
|
334
351
|
});
|
|
@@ -10,6 +10,15 @@ Vue.use(PortalVue);
|
|
|
10
10
|
const staticOptions = [
|
|
11
11
|
{ icon: 'eye-slash', value: true, title: 'Yes' },
|
|
12
12
|
{ icon: 'eye', value: false, title: 'No' },
|
|
13
|
+
{
|
|
14
|
+
icon: 'pencil',
|
|
15
|
+
value: 'custom',
|
|
16
|
+
title: 'Custom',
|
|
17
|
+
component: {
|
|
18
|
+
props: ['option'],
|
|
19
|
+
template: '<span>{{ option.title }} <b class="gl-text-red-500">component</b></span>',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
13
22
|
];
|
|
14
23
|
|
|
15
24
|
const generateProps = ({ active = true } = {}) => ({
|
|
@@ -4,7 +4,7 @@ import { Portal } from 'portal-vue';
|
|
|
4
4
|
import { COMMA, LEFT_MOUSE_BUTTON } from '../../../utils/constants';
|
|
5
5
|
import GlFilteredSearchSuggestion from './filtered_search_suggestion.vue';
|
|
6
6
|
import GlFilteredSearchSuggestionList from './filtered_search_suggestion_list.vue';
|
|
7
|
-
import { splitOnQuotes, wrapTokenInQuotes } from './filtered_search_utils';
|
|
7
|
+
import { splitOnQuotes, wrapTokenInQuotes, match } from './filtered_search_utils';
|
|
8
8
|
|
|
9
9
|
// We need some helpers to ensure @vue/compat compatibility
|
|
10
10
|
// @vue/compat will render comment nodes for v-if and comments in HTML
|
|
@@ -62,6 +62,11 @@ export default {
|
|
|
62
62
|
required: false,
|
|
63
63
|
default: false,
|
|
64
64
|
},
|
|
65
|
+
isTerm: {
|
|
66
|
+
type: Boolean,
|
|
67
|
+
required: false,
|
|
68
|
+
default: false,
|
|
69
|
+
},
|
|
65
70
|
label: {
|
|
66
71
|
type: String,
|
|
67
72
|
required: false,
|
|
@@ -80,7 +85,7 @@ export default {
|
|
|
80
85
|
optionTextField: {
|
|
81
86
|
type: String,
|
|
82
87
|
required: false,
|
|
83
|
-
default: '
|
|
88
|
+
default: 'title',
|
|
84
89
|
},
|
|
85
90
|
customInputKeydownHandler: {
|
|
86
91
|
type: Function,
|
|
@@ -145,18 +150,22 @@ export default {
|
|
|
145
150
|
|
|
146
151
|
inputValue: {
|
|
147
152
|
get() {
|
|
153
|
+
if (this.isTerm) {
|
|
154
|
+
return this.nonMultipleValue;
|
|
155
|
+
}
|
|
156
|
+
|
|
148
157
|
return this.matchingOption
|
|
149
158
|
? this.matchingOption[this.optionTextField]
|
|
150
159
|
: this.nonMultipleValue;
|
|
151
160
|
},
|
|
152
161
|
|
|
153
|
-
set(
|
|
162
|
+
set(inputValue) {
|
|
154
163
|
/**
|
|
155
164
|
* Emitted when this token segment's value changes.
|
|
156
165
|
*
|
|
157
166
|
* @type {object} option The current option.
|
|
158
167
|
*/
|
|
159
|
-
this.$emit('input',
|
|
168
|
+
this.$emit('input', inputValue);
|
|
160
169
|
},
|
|
161
170
|
},
|
|
162
171
|
|
|
@@ -176,8 +185,13 @@ export default {
|
|
|
176
185
|
return option?.value;
|
|
177
186
|
}
|
|
178
187
|
|
|
179
|
-
const
|
|
180
|
-
|
|
188
|
+
const defaultOption = this.options.find((op) => op.default);
|
|
189
|
+
|
|
190
|
+
if (defaultOption) {
|
|
191
|
+
return defaultOption.value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return this.isTerm ? undefined : this.options[0]?.value;
|
|
181
195
|
},
|
|
182
196
|
containerAttributes() {
|
|
183
197
|
return (
|
|
@@ -210,7 +224,7 @@ export default {
|
|
|
210
224
|
const [firstWord, ...otherWords] = splitOnQuotes(newValue).filter(
|
|
211
225
|
(w, idx, arr) => Boolean(w) || idx === arr.length - 1
|
|
212
226
|
);
|
|
213
|
-
this.$emit('input',
|
|
227
|
+
this.$emit('input', firstWord);
|
|
214
228
|
|
|
215
229
|
if (otherWords.length) {
|
|
216
230
|
/**
|
|
@@ -233,9 +247,11 @@ export default {
|
|
|
233
247
|
}
|
|
234
248
|
},
|
|
235
249
|
|
|
236
|
-
getMatchingOptionForInputValue(
|
|
237
|
-
return this.options?.find((
|
|
238
|
-
loose
|
|
250
|
+
getMatchingOptionForInputValue(inputValue, { loose } = { loose: false }) {
|
|
251
|
+
return this.options?.find((option) =>
|
|
252
|
+
loose
|
|
253
|
+
? match(option[this.optionTextField], inputValue)
|
|
254
|
+
: option[this.optionTextField] === inputValue
|
|
239
255
|
);
|
|
240
256
|
},
|
|
241
257
|
|
|
@@ -256,7 +272,7 @@ export default {
|
|
|
256
272
|
},
|
|
257
273
|
|
|
258
274
|
deactivate() {
|
|
259
|
-
if (!this.options) {
|
|
275
|
+
if (!this.options || this.isTerm) {
|
|
260
276
|
return;
|
|
261
277
|
}
|
|
262
278
|
|
|
@@ -410,7 +426,14 @@ export default {
|
|
|
410
426
|
:value="option.value"
|
|
411
427
|
:icon-name="option.icon"
|
|
412
428
|
>
|
|
413
|
-
<slot name="option" v-bind="{ option }">
|
|
429
|
+
<slot name="option" v-bind="{ option }">
|
|
430
|
+
<template v-if="option.component">
|
|
431
|
+
<component :is="option.component" :option="option" />
|
|
432
|
+
</template>
|
|
433
|
+
<template v-else>
|
|
434
|
+
{{ option[optionTextField] }}
|
|
435
|
+
</template>
|
|
436
|
+
</slot>
|
|
414
437
|
</gl-filtered-search-suggestion>
|
|
415
438
|
</template>
|
|
416
439
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import first from 'lodash/first';
|
|
2
2
|
import last from 'lodash/last';
|
|
3
3
|
import isString from 'lodash/isString';
|
|
4
|
+
import { modulo } from '../../../utils/number_utils';
|
|
4
5
|
|
|
5
6
|
export const TERM_TOKEN_TYPE = 'filtered-search-term';
|
|
6
7
|
|
|
@@ -44,6 +45,61 @@ export function needDenormalization(tokens) {
|
|
|
44
45
|
return tokens.some((t) => typeof t === 'string' || !t.id);
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Given an initial index, step size and array length, returns an index that is
|
|
50
|
+
* within the array bounds (unless step is 0; see † below).
|
|
51
|
+
*
|
|
52
|
+
* The step can be any positive or negative integer, including zero.
|
|
53
|
+
*
|
|
54
|
+
* An out-of-bounds index is considered 'uninitialised', and is handled
|
|
55
|
+
* specially. For instance, the 'next' index of 'uninitialised' is the first
|
|
56
|
+
* index:
|
|
57
|
+
*
|
|
58
|
+
* stepIndexAndWrap(-1, 1, 5) === 0
|
|
59
|
+
*
|
|
60
|
+
* The 'previous' index of 'uninitialised' is the last index:
|
|
61
|
+
*
|
|
62
|
+
* stepIndexAndWrap(-1, -1, 5) === 4
|
|
63
|
+
*
|
|
64
|
+
* †: If step is 0, the index is returned as-is, which may be out-of-bounds.
|
|
65
|
+
*
|
|
66
|
+
* @param {number} index The initial index.
|
|
67
|
+
* @param {number} step The amount to step by (positive or negative).
|
|
68
|
+
* @param {number} length The length of the array.
|
|
69
|
+
* @returns {number}
|
|
70
|
+
*/
|
|
71
|
+
export function stepIndexAndWrap(index, step, length) {
|
|
72
|
+
if (step === 0) return index;
|
|
73
|
+
|
|
74
|
+
let start;
|
|
75
|
+
const indexInRange = index >= 0 && index < length;
|
|
76
|
+
|
|
77
|
+
if (indexInRange) {
|
|
78
|
+
// Step from the valid index.
|
|
79
|
+
start = index;
|
|
80
|
+
} else if (step > 0) {
|
|
81
|
+
// Step forwards from the beginning of the array.
|
|
82
|
+
start = -1;
|
|
83
|
+
} else {
|
|
84
|
+
// Step backwards from the end of the array.
|
|
85
|
+
start = length;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return modulo(start + step, length);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Transforms a given token definition to an option definition.
|
|
93
|
+
*
|
|
94
|
+
* @param {Object} token A token definition (see GlFilteredSearch's
|
|
95
|
+
* availableTokens prop).
|
|
96
|
+
* @returns {Object} A option definition (see GlFilteredSearchTokenSegment's
|
|
97
|
+
* options prop).
|
|
98
|
+
*/
|
|
99
|
+
export function tokenToOption({ icon, title, type, optionComponent }) {
|
|
100
|
+
return { icon, title, value: type, component: optionComponent };
|
|
101
|
+
}
|
|
102
|
+
|
|
47
103
|
let tokenIdCounter = 0;
|
|
48
104
|
const getTokenId = () => {
|
|
49
105
|
const tokenId = `token-${tokenIdCounter}`;
|
|
@@ -96,6 +152,19 @@ export function denormalizeTokens(inputTokens) {
|
|
|
96
152
|
return result;
|
|
97
153
|
}
|
|
98
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Returns `true` if `text` contains `query` (case insensitive).
|
|
157
|
+
*
|
|
158
|
+
* This is used in `filter` and `find` array methods for token segment options.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} text The string to look within.
|
|
161
|
+
* @param {string} query The string to find inside the text.
|
|
162
|
+
* @returns {boolean}
|
|
163
|
+
*/
|
|
164
|
+
export function match(text, query) {
|
|
165
|
+
return text.toLowerCase().includes(query.toLowerCase());
|
|
166
|
+
}
|
|
167
|
+
|
|
99
168
|
export function splitOnQuotes(str) {
|
|
100
169
|
if (first(str) === "'" && last(str) === "'") {
|
|
101
170
|
return [str];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { splitOnQuotes, wrapTokenInQuotes } from './filtered_search_utils';
|
|
1
|
+
import { splitOnQuotes, wrapTokenInQuotes, stepIndexAndWrap } from './filtered_search_utils';
|
|
2
2
|
|
|
3
3
|
describe('FilteredSearchUtils', () => {
|
|
4
4
|
describe('splitOnQuotes', () => {
|
|
@@ -48,4 +48,35 @@ describe('FilteredSearchUtils', () => {
|
|
|
48
48
|
expect(wrapTokenInQuotes(token)).toEqual(`"${token}"`);
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
describe('stepIndexAndWrap', () => {
|
|
53
|
+
it.each`
|
|
54
|
+
index | step | length | result
|
|
55
|
+
${0} | ${0} | ${5} | ${0}
|
|
56
|
+
${0} | ${1} | ${5} | ${1}
|
|
57
|
+
${0} | ${-1} | ${5} | ${4}
|
|
58
|
+
${0} | ${6} | ${5} | ${1}
|
|
59
|
+
${0} | ${-6} | ${5} | ${4}
|
|
60
|
+
${1} | ${0} | ${5} | ${1}
|
|
61
|
+
${1} | ${1} | ${5} | ${2}
|
|
62
|
+
${1} | ${-2} | ${5} | ${4}
|
|
63
|
+
${1} | ${-5} | ${5} | ${1}
|
|
64
|
+
${-1} | ${0} | ${5} | ${-1}
|
|
65
|
+
${6} | ${0} | ${5} | ${6}
|
|
66
|
+
${6} | ${1} | ${5} | ${0}
|
|
67
|
+
${6} | ${-1} | ${5} | ${4}
|
|
68
|
+
${-1} | ${1} | ${5} | ${0}
|
|
69
|
+
${-1} | ${-1} | ${5} | ${4}
|
|
70
|
+
${-1} | ${-5} | ${5} | ${0}
|
|
71
|
+
${-1} | ${5} | ${5} | ${4}
|
|
72
|
+
${4} | ${1} | ${5} | ${0}
|
|
73
|
+
${4} | ${2} | ${5} | ${1}
|
|
74
|
+
${NaN} | ${1} | ${1} | ${0}
|
|
75
|
+
${0} | ${NaN} | ${1} | ${NaN}
|
|
76
|
+
${0} | ${1} | ${NaN} | ${NaN}
|
|
77
|
+
${0} | ${1} | ${0} | ${NaN}
|
|
78
|
+
`('stepIndex($index, $step, $length) === $result', ({ index, step, length, result }) => {
|
|
79
|
+
expect(stepIndexAndWrap(index, step, length, result)).toBe(result);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
51
82
|
});
|
|
@@ -17,6 +17,27 @@ export const sum = (...numbers) => numbers.reduce(addition);
|
|
|
17
17
|
*/
|
|
18
18
|
export const average = (...numbers) => sum(...numbers) / numbers.length;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Returns the modulo of n for a divisor.
|
|
22
|
+
*
|
|
23
|
+
* Maps the integer n into the range [0, divisor) when the divisor is positive,
|
|
24
|
+
* and (divisor, 0] when the divisor is negative.
|
|
25
|
+
*
|
|
26
|
+
* This is useful when indexing into an array, to ensure you always stay within
|
|
27
|
+
* the array bounds.
|
|
28
|
+
*
|
|
29
|
+
* See https://2ality.com/2019/08/remainder-vs-modulo.html.
|
|
30
|
+
*
|
|
31
|
+
* @param {number} n The number to mod.
|
|
32
|
+
* @param {number} divisor The divisor (e.g., the length of an array).
|
|
33
|
+
* @returns {number}
|
|
34
|
+
*/
|
|
35
|
+
export function modulo(n, divisor) {
|
|
36
|
+
const result = ((n % divisor) + divisor) % divisor;
|
|
37
|
+
// Never return -0.
|
|
38
|
+
return result === 0 ? 0 : result;
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
/**
|
|
21
42
|
* Convert number to engineering format, using SI suffix
|
|
22
43
|
* @param {Number|String} value - Number or Number-convertible String
|
|
@@ -44,4 +44,46 @@ describe('number utils', () => {
|
|
|
44
44
|
expect(numberUtils.engineeringNotation(...input)).toBe(output);
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
describe('modulo', () => {
|
|
49
|
+
it.each`
|
|
50
|
+
n | divisor | result
|
|
51
|
+
${-7} | ${3} | ${2}
|
|
52
|
+
${-6} | ${3} | ${0}
|
|
53
|
+
${-5} | ${3} | ${1}
|
|
54
|
+
${-4} | ${3} | ${2}
|
|
55
|
+
${-3} | ${3} | ${0}
|
|
56
|
+
${-2} | ${3} | ${1}
|
|
57
|
+
${-1} | ${3} | ${2}
|
|
58
|
+
${0} | ${3} | ${0}
|
|
59
|
+
${1} | ${3} | ${1}
|
|
60
|
+
${2} | ${3} | ${2}
|
|
61
|
+
${3} | ${3} | ${0}
|
|
62
|
+
${4} | ${3} | ${1}
|
|
63
|
+
${5} | ${3} | ${2}
|
|
64
|
+
${6} | ${3} | ${0}
|
|
65
|
+
${7} | ${3} | ${1}
|
|
66
|
+
${-7} | ${-3} | ${-1}
|
|
67
|
+
${-6} | ${-3} | ${0}
|
|
68
|
+
${-5} | ${-3} | ${-2}
|
|
69
|
+
${-4} | ${-3} | ${-1}
|
|
70
|
+
${-3} | ${-3} | ${0}
|
|
71
|
+
${-2} | ${-3} | ${-2}
|
|
72
|
+
${-1} | ${-3} | ${-1}
|
|
73
|
+
${0} | ${-3} | ${0}
|
|
74
|
+
${1} | ${-3} | ${-2}
|
|
75
|
+
${2} | ${-3} | ${-1}
|
|
76
|
+
${3} | ${-3} | ${0}
|
|
77
|
+
${4} | ${-3} | ${-2}
|
|
78
|
+
${5} | ${-3} | ${-1}
|
|
79
|
+
${6} | ${-3} | ${0}
|
|
80
|
+
${7} | ${-3} | ${-2}
|
|
81
|
+
${NaN} | ${1} | ${NaN}
|
|
82
|
+
${1} | ${NaN} | ${NaN}
|
|
83
|
+
${1} | ${0} | ${NaN}
|
|
84
|
+
${Infinity} | ${1} | ${NaN}
|
|
85
|
+
`('modulo($n, $divisor) === $result', ({ n, divisor, result }) => {
|
|
86
|
+
expect(numberUtils.modulo(n, divisor)).toBe(result);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
47
89
|
});
|