summaryrefslogtreecommitdiff
path: root/spec/frontend/lib/utils
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/lib/utils')
-rw-r--r--spec/frontend/lib/utils/axios_startup_calls_spec.js7
-rw-r--r--spec/frontend/lib/utils/chart_utils_spec.js55
-rw-r--r--spec/frontend/lib/utils/color_utils_spec.js42
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js8
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js3
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js18
-rw-r--r--spec/frontend/lib/utils/css_utils_spec.js22
-rw-r--r--spec/frontend/lib/utils/datetime/date_format_utility_spec.js12
-rw-r--r--spec/frontend/lib/utils/datetime/time_spent_utility_spec.js25
-rw-r--r--spec/frontend/lib/utils/datetime/timeago_utility_spec.js37
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js40
-rw-r--r--spec/frontend/lib/utils/error_message_spec.js48
-rw-r--r--spec/frontend/lib/utils/file_upload_spec.js22
-rw-r--r--spec/frontend/lib/utils/intersection_observer_spec.js2
-rw-r--r--spec/frontend/lib/utils/number_utility_spec.js29
-rw-r--r--spec/frontend/lib/utils/poll_spec.js2
-rw-r--r--spec/frontend/lib/utils/ref_validator_spec.js79
-rw-r--r--spec/frontend/lib/utils/secret_detection_spec.js68
-rw-r--r--spec/frontend/lib/utils/sticky_spec.js77
-rw-r--r--spec/frontend/lib/utils/tappable_promise_spec.js63
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js62
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js32
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js20
-rw-r--r--spec/frontend/lib/utils/vuex_module_mappers_spec.js4
-rw-r--r--spec/frontend/lib/utils/web_ide_navigator_spec.js38
25 files changed, 614 insertions, 201 deletions
diff --git a/spec/frontend/lib/utils/axios_startup_calls_spec.js b/spec/frontend/lib/utils/axios_startup_calls_spec.js
index 4471b781446..3d063ff9b46 100644
--- a/spec/frontend/lib/utils/axios_startup_calls_spec.js
+++ b/spec/frontend/lib/utils/axios_startup_calls_spec.js
@@ -113,17 +113,10 @@ describe('setupAxiosStartupCalls', () => {
});
describe('startup call', () => {
- let oldGon;
-
beforeEach(() => {
- oldGon = window.gon;
window.gon = { gitlab_url: 'https://example.org/gitlab' };
});
- afterEach(() => {
- window.gon = oldGon;
- });
-
it('removes GitLab Base URL from startup call', async () => {
window.gl.startup_calls = {
'/startup': {
diff --git a/spec/frontend/lib/utils/chart_utils_spec.js b/spec/frontend/lib/utils/chart_utils_spec.js
index 65bb68c5017..3b34b0ef672 100644
--- a/spec/frontend/lib/utils/chart_utils_spec.js
+++ b/spec/frontend/lib/utils/chart_utils_spec.js
@@ -1,4 +1,8 @@
-import { firstAndLastY } from '~/lib/utils/chart_utils';
+import { firstAndLastY, getToolboxOptions } from '~/lib/utils/chart_utils';
+import { __ } from '~/locale';
+import * as iconUtils from '~/lib/utils/icon_utils';
+
+jest.mock('~/lib/utils/icon_utils');
describe('Chart utils', () => {
describe('firstAndLastY', () => {
@@ -12,4 +16,53 @@ describe('Chart utils', () => {
expect(firstAndLastY(data)).toEqual([1, 3]);
});
});
+
+ describe('getToolboxOptions', () => {
+ describe('when icons are successfully fetched', () => {
+ beforeEach(() => {
+ iconUtils.getSvgIconPathContent.mockImplementation((name) =>
+ Promise.resolve(`${name}-svg-path-mock`),
+ );
+ });
+
+ it('returns toolbox config', async () => {
+ await expect(getToolboxOptions()).resolves.toEqual({
+ toolbox: {
+ feature: {
+ dataZoom: {
+ icon: {
+ zoom: 'path://marquee-selection-svg-path-mock',
+ back: 'path://redo-svg-path-mock',
+ },
+ },
+ restore: {
+ icon: 'path://repeat-svg-path-mock',
+ },
+ saveAsImage: {
+ icon: 'path://download-svg-path-mock',
+ },
+ },
+ },
+ });
+ });
+ });
+
+ describe('when icons are not successfully fetched', () => {
+ const error = new Error();
+
+ beforeEach(() => {
+ iconUtils.getSvgIconPathContent.mockRejectedValue(error);
+ jest.spyOn(console, 'warn').mockImplementation();
+ });
+
+ it('returns empty object and calls `console.warn`', async () => {
+ await expect(getToolboxOptions()).resolves.toEqual({});
+ // eslint-disable-next-line no-console
+ expect(console.warn).toHaveBeenCalledWith(
+ __('SVG could not be rendered correctly: '),
+ error,
+ );
+ });
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/color_utils_spec.js b/spec/frontend/lib/utils/color_utils_spec.js
index 87966cf9fba..92ac66c19f0 100644
--- a/spec/frontend/lib/utils/color_utils_spec.js
+++ b/spec/frontend/lib/utils/color_utils_spec.js
@@ -1,44 +1,6 @@
-import {
- isValidColorExpression,
- textColorForBackground,
- hexToRgb,
- validateHexColor,
- darkModeEnabled,
-} from '~/lib/utils/color_utils';
+import { isValidColorExpression, validateHexColor, darkModeEnabled } from '~/lib/utils/color_utils';
describe('Color utils', () => {
- describe('Converting hex code to rgb', () => {
- it('convert hex code to rgb', () => {
- expect(hexToRgb('#000000')).toEqual([0, 0, 0]);
- expect(hexToRgb('#ffffff')).toEqual([255, 255, 255]);
- });
-
- it('convert short hex code to rgb', () => {
- expect(hexToRgb('#000')).toEqual([0, 0, 0]);
- expect(hexToRgb('#fff')).toEqual([255, 255, 255]);
- });
-
- it('handle conversion regardless of the characters case', () => {
- expect(hexToRgb('#f0F')).toEqual([255, 0, 255]);
- });
- });
-
- describe('Getting text color for given background', () => {
- // following tests are being ported from `text_color_for_bg` section in labels_helper_spec.rb
- it('uses light text on dark backgrounds', () => {
- expect(textColorForBackground('#222E2E')).toEqual('#FFFFFF');
- });
-
- it('uses dark text on light backgrounds', () => {
- expect(textColorForBackground('#EEEEEE')).toEqual('#333333');
- });
-
- it('supports RGB triplets', () => {
- expect(textColorForBackground('#FFF')).toEqual('#333333');
- expect(textColorForBackground('#000')).toEqual('#FFFFFF');
- });
- });
-
describe('Validate hex color', () => {
it.each`
color | output
@@ -63,7 +25,7 @@ describe('Color utils', () => {
${'groups:issues:index'} | ${'gl-dark'} | ${'monokai-light'} | ${true}
`(
'is $expected on $page with $bodyClass body class and $ideTheme IDE theme',
- async ({ page, bodyClass, ideTheme, expected }) => {
+ ({ page, bodyClass, ideTheme, expected }) => {
document.body.outerHTML = `<body class="${bodyClass}" data-page="${page}"></body>`;
window.gon = {
user_color_scheme: ideTheme,
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 7b068f7d248..b4ec00ab766 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -534,18 +534,10 @@ describe('common_utils', () => {
});
describe('spriteIcon', () => {
- let beforeGon;
-
beforeEach(() => {
- window.gon = window.gon || {};
- beforeGon = { ...window.gon };
window.gon.sprite_icons = 'icons.svg';
});
- afterEach(() => {
- window.gon = beforeGon;
- });
-
it('should return the svg for a linked icon', () => {
expect(commonUtils.spriteIcon('test')).toEqual(
'<svg ><use xlink:href="icons.svg#test" /></svg>',
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
index 142c76f7bc0..fab5a7b8844 100644
--- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
@@ -44,7 +44,6 @@ describe('confirmAction', () => {
resetHTMLFixture();
Vue.prototype.$mount.mockRestore();
modalWrapper?.destroy();
- modalWrapper = null;
modal?.destroy();
modal = null;
});
@@ -67,6 +66,7 @@ describe('confirmAction', () => {
modalHtmlMessage: '<strong>Hello</strong>',
title: 'title',
hideCancel: true,
+ size: 'md',
};
await renderRootComponent('', options);
expect(modal.props()).toEqual(
@@ -80,6 +80,7 @@ describe('confirmAction', () => {
modalHtmlMessage: options.modalHtmlMessage,
title: options.title,
hideCancel: options.hideCancel,
+ size: 'md',
}),
);
});
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
index 313e028d861..9dcb850076c 100644
--- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
@@ -14,6 +14,7 @@ describe('Confirm Modal', () => {
secondaryText,
secondaryVariant,
title,
+ size,
hideCancel = false,
} = {}) => {
wrapper = mount(ConfirmModal, {
@@ -24,14 +25,11 @@ describe('Confirm Modal', () => {
secondaryVariant,
hideCancel,
title,
+ size,
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlModal = () => wrapper.findComponent(GlModal);
describe('Modal events', () => {
@@ -95,5 +93,17 @@ describe('Confirm Modal', () => {
expect(findGlModal().props().title).toBe(title);
});
+
+ it('should set modal size to `sm` by default', () => {
+ createComponent();
+
+ expect(findGlModal().props('size')).toBe('sm');
+ });
+
+ it('should set modal size when `size` prop is set', () => {
+ createComponent({ size: 'md' });
+
+ expect(findGlModal().props('size')).toBe('md');
+ });
});
});
diff --git a/spec/frontend/lib/utils/css_utils_spec.js b/spec/frontend/lib/utils/css_utils_spec.js
new file mode 100644
index 00000000000..dcaeb075c93
--- /dev/null
+++ b/spec/frontend/lib/utils/css_utils_spec.js
@@ -0,0 +1,22 @@
+import { getCssClassDimensions } from '~/lib/utils/css_utils';
+
+describe('getCssClassDimensions', () => {
+ const mockDimensions = { width: 1, height: 2 };
+ let actual;
+
+ beforeEach(() => {
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(mockDimensions);
+ actual = getCssClassDimensions('foo bar');
+ });
+
+ it('returns the measured width and height', () => {
+ expect(actual).toEqual(mockDimensions);
+ });
+
+ it('measures an element with the given classes', () => {
+ expect(Element.prototype.getBoundingClientRect).toHaveBeenCalledTimes(1);
+
+ const [tempElement] = Element.prototype.getBoundingClientRect.mock.contexts;
+ expect([...tempElement.classList]).toEqual(['foo', 'bar']);
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
index a83b0ed9fbe..e7a6367eeac 100644
--- a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
@@ -134,18 +134,6 @@ describe('formatTimeAsSummary', () => {
});
});
-describe('durationTimeFormatted', () => {
- it.each`
- duration | expectedOutput
- ${87} | ${'00:01:27'}
- ${141} | ${'00:02:21'}
- ${12} | ${'00:00:12'}
- ${60} | ${'00:01:00'}
- `('returns $expectedOutput when provided $duration', ({ duration, expectedOutput }) => {
- expect(utils.durationTimeFormatted(duration)).toBe(expectedOutput);
- });
-});
-
describe('formatUtcOffset', () => {
it.each`
offset | expected
diff --git a/spec/frontend/lib/utils/datetime/time_spent_utility_spec.js b/spec/frontend/lib/utils/datetime/time_spent_utility_spec.js
new file mode 100644
index 00000000000..15e056e45d0
--- /dev/null
+++ b/spec/frontend/lib/utils/datetime/time_spent_utility_spec.js
@@ -0,0 +1,25 @@
+import { formatTimeSpent } from '~/lib/utils/datetime/time_spent_utility';
+
+describe('Time spent utils', () => {
+ describe('formatTimeSpent', () => {
+ describe('with limitToHours false', () => {
+ it('formats 34500 seconds to `1d 1h 35m`', () => {
+ expect(formatTimeSpent(34500)).toEqual('1d 1h 35m');
+ });
+
+ it('formats -34500 seconds to `- 1d 1h 35m`', () => {
+ expect(formatTimeSpent(-34500)).toEqual('- 1d 1h 35m');
+ });
+ });
+
+ describe('with limitToHours true', () => {
+ it('formats 34500 seconds to `9h 35m`', () => {
+ expect(formatTimeSpent(34500, true)).toEqual('9h 35m');
+ });
+
+ it('formats -34500 seconds to `- 9h 35m`', () => {
+ expect(formatTimeSpent(-34500, true)).toEqual('- 9h 35m');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
index 1ef7047d959..74ce8175357 100644
--- a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
@@ -1,18 +1,9 @@
+import { DATE_ONLY_FORMAT } from '~/lib/utils/datetime/constants';
import { getTimeago, localTimeAgo, timeFor, duration } from '~/lib/utils/datetime/timeago_utility';
import { s__ } from '~/locale';
import '~/commons/bootstrap';
describe('TimeAgo utils', () => {
- let oldGon;
-
- afterEach(() => {
- window.gon = oldGon;
- });
-
- beforeEach(() => {
- oldGon = window.gon;
- });
-
describe('getTimeago', () => {
describe('with User Setting timeDisplayRelative: true', () => {
beforeEach(() => {
@@ -34,15 +25,37 @@ describe('TimeAgo utils', () => {
window.gon = { time_display_relative: false };
});
- it.each([
+ const defaultFormatExpectations = [
[new Date().toISOString(), 'Jul 6, 2020, 12:00 AM'],
[new Date(), 'Jul 6, 2020, 12:00 AM'],
[new Date().getTime(), 'Jul 6, 2020, 12:00 AM'],
// Slightly different behaviour when `null` is passed :see_no_evil`
[null, 'Jan 1, 1970, 12:00 AM'],
- ])('formats date `%p` as `%p`', (date, result) => {
+ ];
+
+ it.each(defaultFormatExpectations)('formats date `%p` as `%p`', (date, result) => {
expect(getTimeago().format(date)).toEqual(result);
});
+
+ describe('with unknown format', () => {
+ it.each(defaultFormatExpectations)(
+ 'uses default format and formats date `%p` as `%p`',
+ (date, result) => {
+ expect(getTimeago('non_existent').format(date)).toEqual(result);
+ },
+ );
+ });
+
+ describe('with DATE_ONLY_FORMAT', () => {
+ it.each([
+ [new Date().toISOString(), 'Jul 6, 2020'],
+ [new Date(), 'Jul 6, 2020'],
+ [new Date().getTime(), 'Jul 6, 2020'],
+ [null, 'Jan 1, 1970'],
+ ])('formats date `%p` as `%p`', (date, result) => {
+ expect(getTimeago(DATE_ONLY_FORMAT).format(date)).toEqual(result);
+ });
+ });
});
});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 8d989350173..330bfca7029 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -276,19 +276,35 @@ describe('getTimeframeWindowFrom', () => {
});
describe('formatTime', () => {
- const expectedTimestamps = [
- [0, '00:00:00'],
- [1000, '00:00:01'],
- [42000, '00:00:42'],
- [121000, '00:02:01'],
- [10921000, '03:02:01'],
- [108000000, '30:00:00'],
- ];
+ it.each`
+ milliseconds | expected
+ ${0} | ${'00:00:00'}
+ ${1} | ${'00:00:00'}
+ ${499} | ${'00:00:00'}
+ ${500} | ${'00:00:01'}
+ ${1000} | ${'00:00:01'}
+ ${42 * 1000} | ${'00:00:42'}
+ ${60 * 1000} | ${'00:01:00'}
+ ${(60 + 1) * 1000} | ${'00:01:01'}
+ ${(3 * 60 * 60 + 2 * 60 + 1) * 1000} | ${'03:02:01'}
+ ${(11 * 60 * 60 + 59 * 60 + 59) * 1000} | ${'11:59:59'}
+ ${30 * 60 * 60 * 1000} | ${'30:00:00'}
+ ${(35 * 60 * 60 + 3 * 60 + 7) * 1000} | ${'35:03:07'}
+ ${240 * 60 * 60 * 1000} | ${'240:00:00'}
+ ${1000 * 60 * 60 * 1000} | ${'1000:00:00'}
+ `(`formats $milliseconds ms as $expected`, ({ milliseconds, expected }) => {
+ expect(datetimeUtility.formatTime(milliseconds)).toBe(expected);
+ });
- expectedTimestamps.forEach(([milliseconds, expectedTimestamp]) => {
- it(`formats ${milliseconds}ms as ${expectedTimestamp}`, () => {
- expect(datetimeUtility.formatTime(milliseconds)).toBe(expectedTimestamp);
- });
+ it.each`
+ milliseconds | expected
+ ${-1} | ${'00:00:00'}
+ ${-499} | ${'00:00:00'}
+ ${-1000} | ${'-00:00:01'}
+ ${-60 * 1000} | ${'-00:01:00'}
+ ${-(35 * 60 * 60 + 3 * 60 + 7) * 1000} | ${'-35:03:07'}
+ `(`when negative, formats $milliseconds ms as $expected`, ({ milliseconds, expected }) => {
+ expect(datetimeUtility.formatTime(milliseconds)).toBe(expected);
});
});
diff --git a/spec/frontend/lib/utils/error_message_spec.js b/spec/frontend/lib/utils/error_message_spec.js
new file mode 100644
index 00000000000..d55a6de06c3
--- /dev/null
+++ b/spec/frontend/lib/utils/error_message_spec.js
@@ -0,0 +1,48 @@
+import { parseErrorMessage } from '~/lib/utils/error_message';
+
+const defaultErrorMessage = 'Default error message';
+const errorMessage = 'Returned error message';
+
+const generateErrorWithMessage = (message) => {
+ return {
+ message,
+ };
+};
+
+describe('parseErrorMessage', () => {
+ const ufErrorPrefix = 'Foo:';
+ beforeEach(() => {
+ gon.uf_error_prefix = ufErrorPrefix;
+ });
+
+ it.each`
+ error | expectedResult
+ ${`${ufErrorPrefix} ${errorMessage}`} | ${errorMessage}
+ ${`${errorMessage} ${ufErrorPrefix}`} | ${defaultErrorMessage}
+ ${errorMessage} | ${defaultErrorMessage}
+ ${undefined} | ${defaultErrorMessage}
+ ${''} | ${defaultErrorMessage}
+ `(
+ 'properly parses "$error" error object and returns "$expectedResult"',
+ ({ error, expectedResult }) => {
+ const errorObject = generateErrorWithMessage(error);
+ expect(parseErrorMessage(errorObject, defaultErrorMessage)).toEqual(expectedResult);
+ },
+ );
+
+ it.each`
+ error | defaultMessage | expectedResult
+ ${undefined} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${''} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${{}} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${generateErrorWithMessage(errorMessage)} | ${undefined} | ${''}
+ ${generateErrorWithMessage(`${ufErrorPrefix} ${errorMessage}`)} | ${undefined} | ${errorMessage}
+ ${generateErrorWithMessage(errorMessage)} | ${''} | ${''}
+ ${generateErrorWithMessage(`${ufErrorPrefix} ${errorMessage}`)} | ${''} | ${errorMessage}
+ `(
+ 'properly handles the edge case of error="$error" and defaultMessage="$defaultMessage"',
+ ({ error, defaultMessage, expectedResult }) => {
+ expect(parseErrorMessage(error, defaultMessage)).toEqual(expectedResult);
+ },
+ );
+});
diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js
index f63af2fe0a4..509ddc7ce86 100644
--- a/spec/frontend/lib/utils/file_upload_spec.js
+++ b/spec/frontend/lib/utils/file_upload_spec.js
@@ -1,5 +1,9 @@
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import fileUpload, { getFilename, validateImageName } from '~/lib/utils/file_upload';
+import fileUpload, {
+ getFilename,
+ validateImageName,
+ validateFileFromAllowList,
+} from '~/lib/utils/file_upload';
describe('File upload', () => {
beforeEach(() => {
@@ -89,3 +93,19 @@ describe('file name validator', () => {
expect(validateImageName(file)).toBe('image.png');
});
});
+
+describe('validateFileFromAllowList', () => {
+ it('returns true if the file type is in the allowed list', () => {
+ const allowList = ['.foo', '.bar'];
+ const fileName = 'file.foo';
+
+ expect(validateFileFromAllowList(fileName, allowList)).toBe(true);
+ });
+
+ it('returns false if the file type is in the allowed list', () => {
+ const allowList = ['.foo', '.bar'];
+ const fileName = 'file.baz';
+
+ expect(validateFileFromAllowList(fileName, allowList)).toBe(false);
+ });
+});
diff --git a/spec/frontend/lib/utils/intersection_observer_spec.js b/spec/frontend/lib/utils/intersection_observer_spec.js
index 71b1daffe0d..8eef403f0ae 100644
--- a/spec/frontend/lib/utils/intersection_observer_spec.js
+++ b/spec/frontend/lib/utils/intersection_observer_spec.js
@@ -57,7 +57,7 @@ describe('IntersectionObserver Utility', () => {
${true} | ${'IntersectionAppear'}
`(
'should emit the correct event on the entry target based on the computed Intersection',
- async ({ isIntersecting, event }) => {
+ ({ isIntersecting, event }) => {
const target = document.createElement('div');
observer.addEntry({ target, isIntersecting });
diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js
index dc4aa0ea5ed..d2591cd2328 100644
--- a/spec/frontend/lib/utils/number_utility_spec.js
+++ b/spec/frontend/lib/utils/number_utility_spec.js
@@ -3,6 +3,7 @@ import {
bytesToKiB,
bytesToMiB,
bytesToGiB,
+ numberToHumanSizeSplit,
numberToHumanSize,
numberToMetricPrefix,
sum,
@@ -13,6 +14,12 @@ import {
isNumeric,
isPositiveInteger,
} from '~/lib/utils/number_utils';
+import {
+ BYTES_FORMAT_BYTES,
+ BYTES_FORMAT_KIB,
+ BYTES_FORMAT_MIB,
+ BYTES_FORMAT_GIB,
+} from '~/lib/utils/constants';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
@@ -78,6 +85,28 @@ describe('Number Utils', () => {
});
});
+ describe('numberToHumanSizeSplit', () => {
+ it('should return bytes', () => {
+ expect(numberToHumanSizeSplit(654)).toEqual(['654', BYTES_FORMAT_BYTES]);
+ expect(numberToHumanSizeSplit(-654)).toEqual(['-654', BYTES_FORMAT_BYTES]);
+ });
+
+ it('should return KiB', () => {
+ expect(numberToHumanSizeSplit(1079)).toEqual(['1.05', BYTES_FORMAT_KIB]);
+ expect(numberToHumanSizeSplit(-1079)).toEqual(['-1.05', BYTES_FORMAT_KIB]);
+ });
+
+ it('should return MiB', () => {
+ expect(numberToHumanSizeSplit(10485764)).toEqual(['10.00', BYTES_FORMAT_MIB]);
+ expect(numberToHumanSizeSplit(-10485764)).toEqual(['-10.00', BYTES_FORMAT_MIB]);
+ });
+
+ it('should return GiB', () => {
+ expect(numberToHumanSizeSplit(10737418240)).toEqual(['10.00', BYTES_FORMAT_GIB]);
+ expect(numberToHumanSizeSplit(-10737418240)).toEqual(['-10.00', BYTES_FORMAT_GIB]);
+ });
+ });
+
describe('numberToHumanSize', () => {
it('should return bytes', () => {
expect(numberToHumanSize(654)).toEqual('654 bytes');
diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js
index 63eeb54e850..096a92305dc 100644
--- a/spec/frontend/lib/utils/poll_spec.js
+++ b/spec/frontend/lib/utils/poll_spec.js
@@ -121,7 +121,7 @@ describe('Poll', () => {
});
describe('with delayed initial request', () => {
- it('delays the first request', async () => {
+ it('delays the first request', () => {
mockServiceCall({ status: HTTP_STATUS_OK, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
diff --git a/spec/frontend/lib/utils/ref_validator_spec.js b/spec/frontend/lib/utils/ref_validator_spec.js
new file mode 100644
index 00000000000..7185ebf0a24
--- /dev/null
+++ b/spec/frontend/lib/utils/ref_validator_spec.js
@@ -0,0 +1,79 @@
+import { validateTag, validationMessages } from '~/lib/utils/ref_validator';
+
+describe('~/lib/utils/ref_validator', () => {
+ describe('validateTag', () => {
+ describe.each([
+ ['foo'],
+ ['FOO'],
+ ['foo/a.lockx'],
+ ['foo.123'],
+ ['foo/123'],
+ ['foo/bar/123'],
+ ['foo.bar.123'],
+ ['foo-bar_baz'],
+ ['head'],
+ ['"foo"-'],
+ ['foo@bar'],
+ ['\ud83e\udd8a'],
+ ['ünicöde'],
+ ['\x80}'],
+ ])('tag with the name "%s"', (tagName) => {
+ it('is valid', () => {
+ const result = validateTag(tagName);
+ expect(result.isValid).toBe(true);
+ expect(result.validationErrors).toEqual([]);
+ });
+ });
+
+ describe.each([
+ [' ', validationMessages.EmptyNameValidationMessage],
+
+ ['refs/heads/tagName', validationMessages.DisallowedPrefixesValidationMessage],
+ ['/foo', validationMessages.DisallowedPrefixesValidationMessage],
+ ['-tagName', validationMessages.DisallowedPrefixesValidationMessage],
+
+ ['HEAD', validationMessages.DisallowedNameValidationMessage],
+ ['@', validationMessages.DisallowedNameValidationMessage],
+
+ ['tag name with spaces', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag\\name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag^name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag..name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['..', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag?name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag*name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag[name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag@{name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag:name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag~name', validationMessages.DisallowedSubstringsValidationMessage],
+
+ ['/', validationMessages.DisallowedSequenceEmptyValidationMessage],
+ ['//', validationMessages.DisallowedSequenceEmptyValidationMessage],
+ ['foo//123', validationMessages.DisallowedSequenceEmptyValidationMessage],
+
+ ['.', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['/./', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['./.', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['.tagName', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['tag/.Name', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['foo/.123/bar', validationMessages.DisallowedSequencePrefixesValidationMessage],
+
+ ['foo.', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['a.lock', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['foo/a.lock', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['foo/a.lock/b', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['foo.123.', validationMessages.DisallowedSequencePostfixesValidationMessage],
+
+ ['foo/', validationMessages.DisallowedPostfixesValidationMessage],
+
+ ['control-character\x7f', validationMessages.ControlCharactersValidationMessage],
+ ['control-character\x15', validationMessages.ControlCharactersValidationMessage],
+ ])('tag with name "%s"', (tagName, validationMessage) => {
+ it(`should be invalid with validation message "${validationMessage}"`, () => {
+ const result = validateTag(tagName);
+ expect(result.isValid).toBe(false);
+ expect(result.validationErrors).toContain(validationMessage);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/secret_detection_spec.js b/spec/frontend/lib/utils/secret_detection_spec.js
new file mode 100644
index 00000000000..7bde6cc4a8e
--- /dev/null
+++ b/spec/frontend/lib/utils/secret_detection_spec.js
@@ -0,0 +1,68 @@
+import { containsSensitiveToken, confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+
+const mockConfirmAction = ({ confirmed }) => {
+ confirmAction.mockResolvedValueOnce(confirmed);
+};
+
+describe('containsSensitiveToken', () => {
+ describe('when message does not contain sensitive tokens', () => {
+ const nonSensitiveMessages = [
+ 'This is a normal message',
+ '1234567890',
+ '!@#$%^&*()_+',
+ 'https://example.com',
+ ];
+
+ it.each(nonSensitiveMessages)('returns false for message: %s', (message) => {
+ expect(containsSensitiveToken(message)).toBe(false);
+ });
+ });
+
+ describe('when message contains sensitive tokens', () => {
+ const sensitiveMessages = [
+ 'token: glpat-cgyKc1k_AsnEpmP-5fRL',
+ 'token: GlPat-abcdefghijklmnopqrstuvwxyz',
+ 'token: feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ 'https://example.com/feed?feed_token=123456789_abcdefghij',
+ 'glpat-1234567890 and feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ ];
+
+ it.each(sensitiveMessages)('returns true for message: %s', (message) => {
+ expect(containsSensitiveToken(message)).toBe(true);
+ });
+ });
+});
+
+describe('confirmSensitiveAction', () => {
+ afterEach(() => {
+ confirmAction.mockReset();
+ });
+
+ it('should call confirmAction with correct parameters', async () => {
+ const prompt = 'Are you sure you want to delete this item?';
+ const expectedParams = {
+ primaryBtnVariant: 'danger',
+ primaryBtnText: i18n.primaryBtnText,
+ };
+ await confirmSensitiveAction(prompt);
+
+ expect(confirmAction).toHaveBeenCalledWith(prompt, expectedParams);
+ });
+
+ it('should return true when confirmed is true', async () => {
+ mockConfirmAction({ confirmed: true });
+
+ const result = await confirmSensitiveAction();
+ expect(result).toBe(true);
+ });
+
+ it('should return false when confirmed is false', async () => {
+ mockConfirmAction({ confirmed: false });
+
+ const result = await confirmSensitiveAction();
+ expect(result).toBe(false);
+ });
+});
diff --git a/spec/frontend/lib/utils/sticky_spec.js b/spec/frontend/lib/utils/sticky_spec.js
deleted file mode 100644
index ec9e746c838..00000000000
--- a/spec/frontend/lib/utils/sticky_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { setHTMLFixture } from 'helpers/fixtures';
-import { isSticky } from '~/lib/utils/sticky';
-
-const TEST_OFFSET_TOP = 500;
-
-describe('sticky', () => {
- let el;
- let offsetTop;
-
- beforeEach(() => {
- setHTMLFixture(
- `
- <div class="parent">
- <div id="js-sticky"></div>
- </div>
- `,
- );
-
- offsetTop = TEST_OFFSET_TOP;
- el = document.getElementById('js-sticky');
- Object.defineProperty(el, 'offsetTop', {
- get() {
- return offsetTop;
- },
- });
- });
-
- afterEach(() => {
- el = null;
- });
-
- describe('when stuck', () => {
- it('does not remove is-stuck class', () => {
- isSticky(el, 0, el.offsetTop);
- isSticky(el, 0, el.offsetTop);
-
- expect(el.classList.contains('is-stuck')).toBe(true);
- });
-
- it('adds is-stuck class', () => {
- isSticky(el, 0, el.offsetTop);
-
- expect(el.classList.contains('is-stuck')).toBe(true);
- });
-
- it('inserts placeholder element', () => {
- isSticky(el, 0, el.offsetTop, true);
-
- expect(document.querySelector('.sticky-placeholder')).not.toBeNull();
- });
- });
-
- describe('when not stuck', () => {
- it('removes is-stuck class', () => {
- jest.spyOn(el.classList, 'remove');
-
- isSticky(el, 0, el.offsetTop);
- isSticky(el, 0, 0);
-
- expect(el.classList.remove).toHaveBeenCalledWith('is-stuck');
- expect(el.classList.contains('is-stuck')).toBe(false);
- });
-
- it('does not add is-stuck class', () => {
- isSticky(el, 0, 0);
-
- expect(el.classList.contains('is-stuck')).toBe(false);
- });
-
- it('removes placeholder', () => {
- isSticky(el, 0, el.offsetTop, true);
- isSticky(el, 0, 0, true);
-
- expect(document.querySelector('.sticky-placeholder')).toBeNull();
- });
- });
-});
diff --git a/spec/frontend/lib/utils/tappable_promise_spec.js b/spec/frontend/lib/utils/tappable_promise_spec.js
new file mode 100644
index 00000000000..654cd20a9de
--- /dev/null
+++ b/spec/frontend/lib/utils/tappable_promise_spec.js
@@ -0,0 +1,63 @@
+import TappablePromise from '~/lib/utils/tappable_promise';
+
+describe('TappablePromise', () => {
+ it('allows a promise to have a progress indicator', () => {
+ const pseudoProgress = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
+ const progressCallback = jest.fn();
+ const promise = new TappablePromise((tap, resolve) => {
+ pseudoProgress.forEach(tap);
+ resolve('done');
+
+ return 'returned value';
+ });
+
+ return promise
+ .tap(progressCallback)
+ .then((val) => {
+ expect(val).toBe('done');
+ expect(val).not.toBe('returned value');
+
+ expect(progressCallback).toHaveBeenCalledTimes(pseudoProgress.length);
+
+ pseudoProgress.forEach((progress, index) => {
+ expect(progressCallback).toHaveBeenNthCalledWith(index + 1, progress);
+ });
+ })
+ .catch(() => {});
+ });
+
+ it('resolves with the value returned by the callback', () => {
+ const promise = new TappablePromise((tap) => {
+ tap(0.5);
+ return 'test';
+ });
+
+ return promise
+ .tap((progress) => {
+ expect(progress).toBe(0.5);
+ })
+ .then((value) => {
+ expect(value).toBe('test');
+ });
+ });
+
+ it('allows a promise to be rejected', () => {
+ const promise = new TappablePromise((tap, resolve, reject) => {
+ reject(new Error('test error'));
+ });
+
+ return promise.catch((e) => {
+ expect(e.message).toBe('test error');
+ });
+ });
+
+ it('rejects the promise if the callback throws an error', () => {
+ const promise = new TappablePromise(() => {
+ throw new Error('test error');
+ });
+
+ return promise.catch((e) => {
+ expect(e.message).toBe('test error');
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index 7aab1013fc0..2180ea7e6c2 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -1,12 +1,16 @@
import $ from 'jquery';
+import AxiosMockAdapter from 'axios-mock-adapter';
import {
insertMarkdownText,
keypressNoteText,
compositionStartNoteText,
compositionEndNoteText,
updateTextForToolbarBtn,
+ resolveSelectedImage,
} from '~/lib/utils/text_markdown';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import '~/lib/utils/jquery_at_who';
+import axios from '~/lib/utils/axios_utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('init markdown', () => {
@@ -14,6 +18,7 @@ describe('init markdown', () => {
let textArea;
let indentButton;
let outdentButton;
+ let axiosMock;
beforeAll(() => {
setHTMLFixture(
@@ -34,6 +39,14 @@ describe('init markdown', () => {
document.execCommand = jest.fn(() => false);
});
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
afterAll(() => {
resetHTMLFixture();
});
@@ -707,6 +720,55 @@ describe('init markdown', () => {
});
});
+ describe('resolveSelectedImage', () => {
+ const markdownPreviewPath = '/markdown/preview';
+ const imageMarkdown = '![image](/uploads/image.png)';
+ const imageAbsoluteUrl = '/abs/uploads/image.png';
+
+ describe('when textarea cursor is positioned on an image', () => {
+ beforeEach(() => {
+ axiosMock.onPost(markdownPreviewPath, { text: imageMarkdown }).reply(HTTP_STATUS_OK, {
+ body: `
+ <p><a href="${imageAbsoluteUrl}"><img src="${imageAbsoluteUrl}"></a></p>
+ `,
+ });
+ });
+
+ it('returns the image absolute URL, markdown, and filename', async () => {
+ textArea.value = `image ${imageMarkdown}`;
+ textArea.setSelectionRange(8, 8);
+ expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toEqual({
+ imageURL: imageAbsoluteUrl,
+ imageMarkdown,
+ filename: 'image.png',
+ });
+ });
+ });
+
+ describe('when textarea cursor is not positioned on an image', () => {
+ it.each`
+ markdown | selectionRange
+ ${`image ${imageMarkdown}`} | ${[4, 4]}
+ ${`!2 (issue)`} | ${[2, 2]}
+ `('returns null', async ({ markdown, selectionRange }) => {
+ textArea.value = markdown;
+ textArea.setSelectionRange(...selectionRange);
+ expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toBe(null);
+ });
+ });
+
+ describe('when textarea cursor is positioned between images', () => {
+ it('returns null', async () => {
+ const position = imageMarkdown.length + 1;
+
+ textArea.value = `${imageMarkdown}\n\n${imageMarkdown}`;
+ textArea.setSelectionRange(position, position);
+
+ expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toBe(null);
+ });
+ });
+ });
+
describe('Source Editor', () => {
let editor;
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index f2572ca0ad2..71a84d56791 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -398,4 +398,36 @@ describe('text_utility', () => {
expect(textUtils.base64DecodeUnicode('8J+YgA==')).toBe('😀');
});
});
+
+ describe('findInvalidBranchNameCharacters', () => {
+ const invalidChars = [' ', '~', '^', ':', '?', '*', '[', '..', '@{', '\\', '//'];
+ const badBranchName = 'branch-with all these ~ ^ : ? * [ ] \\ // .. @{ } //';
+ const goodBranch = 'branch-with-no-errrors';
+
+ it('returns an array of invalid characters in a branch name', () => {
+ const chars = textUtils.findInvalidBranchNameCharacters(badBranchName);
+ chars.forEach((char) => {
+ expect(invalidChars).toContain(char);
+ });
+ });
+
+ it('returns an empty array with no invalid characters', () => {
+ expect(textUtils.findInvalidBranchNameCharacters(goodBranch)).toEqual([]);
+ });
+ });
+
+ describe('humanizeBranchValidationErrors', () => {
+ it.each`
+ errors | message
+ ${[' ']} | ${"Can't contain spaces"}
+ ${['?', '//', ' ']} | ${"Can't contain spaces, ?, //"}
+ ${['\\', '[', '..']} | ${"Can't contain \\, [, .."}
+ `('returns an $message with $errors', ({ errors, message }) => {
+ expect(textUtils.humanizeBranchValidationErrors(errors)).toEqual(message);
+ });
+
+ it('returns an empty string with no invalid characters', () => {
+ expect(textUtils.humanizeBranchValidationErrors([])).toEqual('');
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 6afdab455a6..0799bc87c8c 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -45,10 +45,6 @@ describe('URL utility', () => {
});
describe('webIDEUrl', () => {
- afterEach(() => {
- gon.relative_url_root = '';
- });
-
it('escapes special characters', () => {
expect(urlUtils.webIDEUrl('/gitlab-org/gitlab-#-foss/merge_requests/1')).toBe(
'/-/ide/project/gitlab-org/gitlab-%23-foss/merge_requests/1',
@@ -505,10 +501,6 @@ describe('URL utility', () => {
gon.gitlab_url = gitlabUrl;
});
- afterEach(() => {
- gon.gitlab_url = '';
- });
-
it.each`
url | urlType | external
${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false}
@@ -796,18 +788,6 @@ describe('URL utility', () => {
});
});
- describe('stripFinalUrlSegment', () => {
- it.each`
- path | expected
- ${'http://fake.domain/twitter/typeahead-js/-/tags/v0.11.0'} | ${'http://fake.domain/twitter/typeahead-js/-/tags/'}
- ${'http://fake.domain/bar/cool/-/nested/content'} | ${'http://fake.domain/bar/cool/-/nested/'}
- ${'http://fake.domain/bar/cool?q="search"'} | ${'http://fake.domain/bar/'}
- ${'http://fake.domain/bar/cool#link-to-something'} | ${'http://fake.domain/bar/'}
- `('stripFinalUrlSegment $path => $expected', ({ path, expected }) => {
- expect(urlUtils.stripFinalUrlSegment(path)).toBe(expected);
- });
- });
-
describe('escapeFileUrl', () => {
it('encodes URL excluding the slashes', () => {
expect(urlUtils.escapeFileUrl('/foo-bar/file.md')).toBe('/foo-bar/file.md');
diff --git a/spec/frontend/lib/utils/vuex_module_mappers_spec.js b/spec/frontend/lib/utils/vuex_module_mappers_spec.js
index d25a692dfea..abd5095c1d2 100644
--- a/spec/frontend/lib/utils/vuex_module_mappers_spec.js
+++ b/spec/frontend/lib/utils/vuex_module_mappers_spec.js
@@ -96,10 +96,6 @@ describe('~/lib/utils/vuex_module_mappers', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('from module defined by prop', () => {
it('maps state', () => {
expect(getMappedState()).toEqual({
diff --git a/spec/frontend/lib/utils/web_ide_navigator_spec.js b/spec/frontend/lib/utils/web_ide_navigator_spec.js
new file mode 100644
index 00000000000..0f5cd09d50e
--- /dev/null
+++ b/spec/frontend/lib/utils/web_ide_navigator_spec.js
@@ -0,0 +1,38 @@
+import { visitUrl, webIDEUrl } from '~/lib/utils/url_utility';
+import { openWebIDE } from '~/lib/utils/web_ide_navigator';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+ webIDEUrl: jest.fn().mockImplementation((path) => `/-/ide/projects${path}`),
+}));
+
+describe('openWebIDE', () => {
+ it('when called without projectPath throws TypeError and does not call visitUrl', () => {
+ expect(() => {
+ openWebIDE();
+ }).toThrow(new TypeError('projectPath parameter is required'));
+ expect(visitUrl).not.toHaveBeenCalled();
+ });
+
+ it('when called with projectPath and without fileName calls visitUrl with correct path', () => {
+ const params = { projectPath: 'project-path' };
+ const expectedNonIDEPath = `/${params.projectPath}/edit/main/-/`;
+ const expectedIDEPath = `/-/ide/projects${expectedNonIDEPath}`;
+
+ openWebIDE(params.projectPath);
+
+ expect(webIDEUrl).toHaveBeenCalledWith(expectedNonIDEPath);
+ expect(visitUrl).toHaveBeenCalledWith(expectedIDEPath);
+ });
+
+ it('when called with projectPath and fileName calls visitUrl with correct path', () => {
+ const params = { projectPath: 'project-path', fileName: 'README' };
+ const expectedNonIDEPath = `/${params.projectPath}/edit/main/-/${params.fileName}/`;
+ const expectedIDEPath = `/-/ide/projects${expectedNonIDEPath}`;
+
+ openWebIDE(params.projectPath, params.fileName);
+
+ expect(webIDEUrl).toHaveBeenCalledWith(expectedNonIDEPath);
+ expect(visitUrl).toHaveBeenCalledWith(expectedIDEPath);
+ });
+});