diff options
Diffstat (limited to 'spec/frontend/lib')
-rw-r--r-- | spec/frontend/lib/utils/axios_utils_spec.js | 1 | ||||
-rw-r--r-- | spec/frontend/lib/utils/common_utils_spec.js | 2 | ||||
-rw-r--r-- | spec/frontend/lib/utils/csrf_token_spec.js | 57 | ||||
-rw-r--r-- | spec/frontend/lib/utils/downloader_spec.js | 40 | ||||
-rw-r--r-- | spec/frontend/lib/utils/navigation_utility_spec.js | 23 | ||||
-rw-r--r-- | spec/frontend/lib/utils/poll_spec.js | 225 | ||||
-rw-r--r-- | spec/frontend/lib/utils/sticky_spec.js | 77 | ||||
-rw-r--r-- | spec/frontend/lib/utils/text_markdown_spec.js | 8 | ||||
-rw-r--r-- | spec/frontend/lib/utils/url_utility_spec.js | 147 |
9 files changed, 552 insertions, 28 deletions
diff --git a/spec/frontend/lib/utils/axios_utils_spec.js b/spec/frontend/lib/utils/axios_utils_spec.js index d5c39567f06..1585a38ae86 100644 --- a/spec/frontend/lib/utils/axios_utils_spec.js +++ b/spec/frontend/lib/utils/axios_utils_spec.js @@ -11,6 +11,7 @@ describe('axios_utils', () => { mock = new AxiosMockAdapter(axios); mock.onAny('/ok').reply(200); mock.onAny('/err').reply(500); + // eslint-disable-next-line jest/no-standalone-expect expect(axios.countActiveRequests()).toBe(0); }); diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js index 1edfda30fec..c8dc90c9ace 100644 --- a/spec/frontend/lib/utils/common_utils_spec.js +++ b/spec/frontend/lib/utils/common_utils_spec.js @@ -503,7 +503,7 @@ describe('common_utils', () => { beforeEach(() => { window.gon = window.gon || {}; - beforeGon = Object.assign({}, window.gon); + beforeGon = { ...window.gon }; window.gon.sprite_icons = 'icons.svg'; }); diff --git a/spec/frontend/lib/utils/csrf_token_spec.js b/spec/frontend/lib/utils/csrf_token_spec.js new file mode 100644 index 00000000000..1b98ef126e9 --- /dev/null +++ b/spec/frontend/lib/utils/csrf_token_spec.js @@ -0,0 +1,57 @@ +import csrf from '~/lib/utils/csrf'; +import { setHTMLFixture } from 'helpers/fixtures'; + +describe('csrf', () => { + let testContext; + + beforeEach(() => { + testContext = {}; + }); + + beforeEach(() => { + testContext.tokenKey = 'X-CSRF-Token'; + testContext.token = + 'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ=='; + }); + + it('returns the correct headerKey', () => { + expect(csrf.headerKey).toBe(testContext.tokenKey); + }); + + describe('when csrf token is in the DOM', () => { + beforeEach(() => { + setHTMLFixture(` + <meta name="csrf-token" content="${testContext.token}"> + `); + + csrf.init(); + }); + + it('returns the csrf token', () => { + expect(csrf.token).toBe(testContext.token); + }); + + it('returns the csrf headers object', () => { + expect(csrf.headers[testContext.tokenKey]).toBe(testContext.token); + }); + }); + + describe('when csrf token is not in the DOM', () => { + beforeEach(() => { + setHTMLFixture(` + <meta name="some-other-token"> + `); + + csrf.init(); + }); + + it('returns null for token', () => { + expect(csrf.token).toBeNull(); + }); + + it('returns empty object for headers', () => { + expect(typeof csrf.headers).toBe('object'); + expect(Object.keys(csrf.headers).length).toBe(0); + }); + }); +}); diff --git a/spec/frontend/lib/utils/downloader_spec.js b/spec/frontend/lib/utils/downloader_spec.js new file mode 100644 index 00000000000..c14cba3a62b --- /dev/null +++ b/spec/frontend/lib/utils/downloader_spec.js @@ -0,0 +1,40 @@ +import downloader from '~/lib/utils/downloader'; + +describe('Downloader', () => { + let a; + + beforeEach(() => { + a = { click: jest.fn() }; + jest.spyOn(document, 'createElement').mockImplementation(() => a); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('when inline file content is provided', () => { + const fileData = 'inline content'; + const fileName = 'test.csv'; + + it('uses the data urls to download the file', () => { + downloader({ fileName, fileData }); + expect(document.createElement).toHaveBeenCalledWith('a'); + expect(a.download).toBe(fileName); + expect(a.href).toBe(`data:text/plain;base64,${fileData}`); + expect(a.click).toHaveBeenCalledTimes(1); + }); + }); + + describe('when an endpoint is provided', () => { + const url = 'https://gitlab.com/test.csv'; + const fileName = 'test.csv'; + + it('uses the endpoint to download the file', () => { + downloader({ fileName, url }); + expect(document.createElement).toHaveBeenCalledWith('a'); + expect(a.download).toBe(fileName); + expect(a.href).toBe(url); + expect(a.click).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/spec/frontend/lib/utils/navigation_utility_spec.js b/spec/frontend/lib/utils/navigation_utility_spec.js new file mode 100644 index 00000000000..88172f38894 --- /dev/null +++ b/spec/frontend/lib/utils/navigation_utility_spec.js @@ -0,0 +1,23 @@ +import findAndFollowLink from '~/lib/utils/navigation_utility'; +import { visitUrl } from '~/lib/utils/url_utility'; + +jest.mock('~/lib/utils/url_utility'); + +describe('findAndFollowLink', () => { + it('visits a link when the selector exists', () => { + const href = '/some/path'; + + setFixtures(`<a class="my-shortcut" href="${href}">link</a>`); + + findAndFollowLink('.my-shortcut'); + + expect(visitUrl).toHaveBeenCalledWith(href); + }); + + it('does not throw an exception when the selector does not exist', () => { + // this should not throw an exception + findAndFollowLink('.this-selector-does-not-exist'); + + expect(visitUrl).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js new file mode 100644 index 00000000000..5ee9738ebf3 --- /dev/null +++ b/spec/frontend/lib/utils/poll_spec.js @@ -0,0 +1,225 @@ +import Poll from '~/lib/utils/poll'; +import { successCodes } from '~/lib/utils/http_status'; +import waitForPromises from 'helpers/wait_for_promises'; + +describe('Poll', () => { + let callbacks; + let service; + + function setup() { + return new Poll({ + resource: service, + method: 'fetch', + successCallback: callbacks.success, + errorCallback: callbacks.error, + notificationCallback: callbacks.notification, + }).makeRequest(); + } + + const mockServiceCall = (response, shouldFail = false) => { + const value = { + ...response, + header: response.header || {}, + }; + + if (shouldFail) { + service.fetch.mockRejectedValue(value); + } else { + service.fetch.mockResolvedValue(value); + } + }; + + const waitForAllCallsToFinish = (waitForCount, successCallback) => { + if (!waitForCount) { + return Promise.resolve().then(successCallback()); + } + + jest.runOnlyPendingTimers(); + + return waitForPromises().then(() => waitForAllCallsToFinish(waitForCount - 1, successCallback)); + }; + + beforeEach(() => { + service = { + fetch: jest.fn(), + }; + callbacks = { + success: jest.fn(), + error: jest.fn(), + notification: jest.fn(), + }; + }); + + it('calls the success callback when no header for interval is provided', done => { + mockServiceCall({ status: 200 }); + setup(); + + waitForAllCallsToFinish(1, () => { + expect(callbacks.success).toHaveBeenCalled(); + expect(callbacks.error).not.toHaveBeenCalled(); + + done(); + }); + }); + + it('calls the error callback when the http request returns an error', done => { + mockServiceCall({ status: 500 }, true); + setup(); + + waitForAllCallsToFinish(1, () => { + expect(callbacks.success).not.toHaveBeenCalled(); + expect(callbacks.error).toHaveBeenCalled(); + + done(); + }); + }); + + it('skips the error callback when request is aborted', done => { + mockServiceCall({ status: 0 }, true); + setup(); + + waitForAllCallsToFinish(1, () => { + expect(callbacks.success).not.toHaveBeenCalled(); + expect(callbacks.error).not.toHaveBeenCalled(); + expect(callbacks.notification).toHaveBeenCalled(); + + done(); + }); + }); + + it('should call the success callback when the interval header is -1', done => { + mockServiceCall({ status: 200, headers: { 'poll-interval': -1 } }); + setup() + .then(() => { + expect(callbacks.success).toHaveBeenCalled(); + expect(callbacks.error).not.toHaveBeenCalled(); + + done(); + }) + .catch(done.fail); + }); + + describe('for 2xx status code', () => { + successCodes.forEach(httpCode => { + it(`starts polling when http status is ${httpCode} and interval header is provided`, done => { + mockServiceCall({ status: httpCode, headers: { 'poll-interval': 1 } }); + + const Polling = new Poll({ + resource: service, + method: 'fetch', + data: { page: 1 }, + successCallback: callbacks.success, + errorCallback: callbacks.error, + }); + + Polling.makeRequest(); + + waitForAllCallsToFinish(2, () => { + Polling.stop(); + + expect(service.fetch.mock.calls).toHaveLength(2); + expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); + expect(callbacks.success).toHaveBeenCalled(); + expect(callbacks.error).not.toHaveBeenCalled(); + + done(); + }); + }); + }); + }); + + describe('stop', () => { + it('stops polling when method is called', done => { + mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } }); + + const Polling = new Poll({ + resource: service, + method: 'fetch', + data: { page: 1 }, + successCallback: () => { + Polling.stop(); + }, + errorCallback: callbacks.error, + }); + + jest.spyOn(Polling, 'stop'); + + Polling.makeRequest(); + + waitForAllCallsToFinish(1, () => { + expect(service.fetch.mock.calls).toHaveLength(1); + expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); + expect(Polling.stop).toHaveBeenCalled(); + + done(); + }); + }); + }); + + describe('enable', () => { + it('should enable polling upon a response', done => { + mockServiceCall({ status: 200 }); + const Polling = new Poll({ + resource: service, + method: 'fetch', + data: { page: 1 }, + successCallback: () => {}, + }); + + Polling.enable({ + data: { page: 4 }, + response: { status: 200, headers: { 'poll-interval': 1 } }, + }); + + waitForAllCallsToFinish(1, () => { + Polling.stop(); + + expect(service.fetch.mock.calls).toHaveLength(1); + expect(service.fetch).toHaveBeenCalledWith({ page: 4 }); + expect(Polling.options.data).toEqual({ page: 4 }); + done(); + }); + }); + }); + + describe('restart', () => { + it('should restart polling when its called', done => { + mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } }); + + const Polling = new Poll({ + resource: service, + method: 'fetch', + data: { page: 1 }, + successCallback: () => { + Polling.stop(); + + // Let's pretend that we asynchronously restart this. + // setTimeout is mocked but this will actually get triggered + // in waitForAllCalssToFinish. + setTimeout(() => { + Polling.restart({ data: { page: 4 } }); + }, 1); + }, + errorCallback: callbacks.error, + }); + + jest.spyOn(Polling, 'stop'); + jest.spyOn(Polling, 'enable'); + jest.spyOn(Polling, 'restart'); + + Polling.makeRequest(); + + waitForAllCallsToFinish(2, () => { + Polling.stop(); + + expect(service.fetch.mock.calls).toHaveLength(2); + expect(service.fetch).toHaveBeenCalledWith({ page: 4 }); + expect(Polling.stop).toHaveBeenCalled(); + expect(Polling.enable).toHaveBeenCalled(); + expect(Polling.restart).toHaveBeenCalled(); + expect(Polling.options.data).toEqual({ page: 4 }); + done(); + }); + }); + }); +}); diff --git a/spec/frontend/lib/utils/sticky_spec.js b/spec/frontend/lib/utils/sticky_spec.js new file mode 100644 index 00000000000..4ad68cc9ff6 --- /dev/null +++ b/spec/frontend/lib/utils/sticky_spec.js @@ -0,0 +1,77 @@ +import { isSticky } from '~/lib/utils/sticky'; +import { setHTMLFixture } from 'helpers/fixtures'; + +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')).toBeTruthy(); + }); + + it('adds is-stuck class', () => { + isSticky(el, 0, el.offsetTop); + + expect(el.classList.contains('is-stuck')).toBeTruthy(); + }); + + 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')).toBeFalsy(); + }); + + 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/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index ba3e4020e66..1d616a7da0b 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -25,7 +25,7 @@ describe('init markdown', () => { insertMarkdownText({ textArea, text: textArea.value, - tag: '*', + tag: '* ', blockTag: null, selected: '', wrap: false, @@ -43,7 +43,7 @@ describe('init markdown', () => { insertMarkdownText({ textArea, text: textArea.value, - tag: '*', + tag: '* ', blockTag: null, selected: '', wrap: false, @@ -61,7 +61,7 @@ describe('init markdown', () => { insertMarkdownText({ textArea, text: textArea.value, - tag: '*', + tag: '* ', blockTag: null, selected: '', wrap: false, @@ -79,7 +79,7 @@ describe('init markdown', () => { insertMarkdownText({ textArea, text: textArea.value, - tag: '*', + tag: '* ', blockTag: null, selected: '', wrap: false, diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 4960895890f..c494033badd 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -91,36 +91,75 @@ describe('URL utility', () => { }); describe('mergeUrlParams', () => { + const { mergeUrlParams } = urlUtils; + it('adds w', () => { - expect(urlUtils.mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag'); - expect(urlUtils.mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag'); - expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1'); - expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe( - 'https://host/path?w=1#frag', - ); + expect(mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag'); + expect(mergeUrlParams({ w: 1 }, '')).toBe('?w=1'); + expect(mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag'); + expect(mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1'); + expect(mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe('https://host/path?w=1#frag'); + expect(mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe('https://h/p?k1=v1&w=1#frag'); + expect(mergeUrlParams({ w: 'null' }, '')).toBe('?w=null'); + }); - expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe( - 'https://h/p?k1=v1&w=1#frag', - ); + it('adds multiple params', () => { + expect(mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag'); }); it('updates w', () => { - expect(urlUtils.mergeUrlParams({ w: 1 }, '?k1=v1&w=0#frag')).toBe('?k1=v1&w=1#frag'); + expect(mergeUrlParams({ w: 2 }, '/path?w=1#frag')).toBe('/path?w=2#frag'); + expect(mergeUrlParams({ w: 2 }, 'https://host/path?w=1')).toBe('https://host/path?w=2'); }); - it('adds multiple params', () => { - expect(urlUtils.mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag'); + it('removes null w', () => { + expect(mergeUrlParams({ w: null }, '?w=1#frag')).toBe('#frag'); + expect(mergeUrlParams({ w: null }, '/path?w=1#frag')).toBe('/path#frag'); + expect(mergeUrlParams({ w: null }, 'https://host/path?w=1')).toBe('https://host/path'); + expect(mergeUrlParams({ w: null }, 'https://host/path?w=1#frag')).toBe( + 'https://host/path#frag', + ); + expect(mergeUrlParams({ w: null }, 'https://h/p?k1=v1&w=1#frag')).toBe( + 'https://h/p?k1=v1#frag', + ); }); - it('adds and updates encoded params', () => { - expect(urlUtils.mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag'); + it('adds and updates encoded param values', () => { + expect(mergeUrlParams({ foo: '&', q: '?' }, '?foo=%23#frag')).toBe('?foo=%26&q=%3F#frag'); + expect(mergeUrlParams({ foo: 'a value' }, '')).toBe('?foo=a%20value'); + expect(mergeUrlParams({ foo: 'a value' }, '?foo=1')).toBe('?foo=a%20value'); + }); + + it('adds and updates encoded param names', () => { + expect(mergeUrlParams({ 'a name': 1 }, '')).toBe('?a%20name=1'); + expect(mergeUrlParams({ 'a name': 2 }, '?a%20name=1')).toBe('?a%20name=2'); + expect(mergeUrlParams({ 'a name': null }, '?a%20name=1')).toBe(''); }); it('treats "+" as "%20"', () => { - expect(urlUtils.mergeUrlParams({ ref: 'bogus' }, '?a=lorem+ipsum&ref=charlie')).toBe( + expect(mergeUrlParams({ ref: 'bogus' }, '?a=lorem+ipsum&ref=charlie')).toBe( '?a=lorem%20ipsum&ref=bogus', ); }); + + it('treats question marks and slashes as part of the query', () => { + expect(mergeUrlParams({ ending: '!' }, '?ending=?&foo=bar')).toBe('?ending=!&foo=bar'); + expect(mergeUrlParams({ ending: '!' }, 'https://host/path?ending=?&foo=bar')).toBe( + 'https://host/path?ending=!&foo=bar', + ); + expect(mergeUrlParams({ ending: '?' }, '?ending=!&foo=bar')).toBe('?ending=%3F&foo=bar'); + expect(mergeUrlParams({ ending: '?' }, 'https://host/path?ending=!&foo=bar')).toBe( + 'https://host/path?ending=%3F&foo=bar', + ); + expect(mergeUrlParams({ ending: '!', op: '+' }, '?ending=?&op=/')).toBe('?ending=!&op=%2B'); + expect(mergeUrlParams({ ending: '!', op: '+' }, 'https://host/path?ending=?&op=/')).toBe( + 'https://host/path?ending=!&op=%2B', + ); + expect(mergeUrlParams({ op: '+' }, '?op=/&foo=bar')).toBe('?op=%2B&foo=bar'); + expect(mergeUrlParams({ op: '+' }, 'https://host/path?op=/&foo=bar')).toBe( + 'https://host/path?op=%2B&foo=bar', + ); + }); }); describe('removeParams', () => { @@ -284,20 +323,76 @@ describe('URL utility', () => { }); }); - describe('isAbsoluteOrRootRelative', () => { - const validUrls = ['https://gitlab.com/', 'http://gitlab.com/', '/users/sign_in']; - - const invalidUrls = [' https://gitlab.com/', './file/path', 'notanurl', '<a></a>']; + describe('isAbsolute', () => { + it.each` + url | valid + ${'https://gitlab.com/'} | ${true} + ${'http://gitlab.com/'} | ${true} + ${'/users/sign_in'} | ${false} + ${' https://gitlab.com'} | ${false} + ${'somepath.php?url=https://gitlab.com'} | ${false} + ${'notaurl'} | ${false} + ${'../relative_url'} | ${false} + ${'<a></a>'} | ${false} + `('returns $valid for $url', ({ url, valid }) => { + expect(urlUtils.isAbsolute(url)).toBe(valid); + }); + }); - it.each(validUrls)(`returns true for %s`, url => { - expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(true); + describe('isRootRelative', () => { + it.each` + url | valid + ${'https://gitlab.com/'} | ${false} + ${'http://gitlab.com/'} | ${false} + ${'/users/sign_in'} | ${true} + ${' https://gitlab.com'} | ${false} + ${'/somepath.php?url=https://gitlab.com'} | ${true} + ${'notaurl'} | ${false} + ${'../relative_url'} | ${false} + ${'<a></a>'} | ${false} + `('returns $valid for $url', ({ url, valid }) => { + expect(urlUtils.isRootRelative(url)).toBe(valid); }); + }); - it.each(invalidUrls)(`returns false for %s`, url => { - expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(false); + describe('isAbsoluteOrRootRelative', () => { + it.each` + url | valid + ${'https://gitlab.com/'} | ${true} + ${'http://gitlab.com/'} | ${true} + ${'/users/sign_in'} | ${true} + ${' https://gitlab.com'} | ${false} + ${'/somepath.php?url=https://gitlab.com'} | ${true} + ${'notaurl'} | ${false} + ${'../relative_url'} | ${false} + ${'<a></a>'} | ${false} + `('returns $valid for $url', ({ url, valid }) => { + expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(valid); }); }); + describe('relativePathToAbsolute', () => { + it.each` + path | base | result + ${'./foo'} | ${'bar/'} | ${'/bar/foo'} + ${'../john.md'} | ${'bar/baz/foo.php'} | ${'/bar/john.md'} + ${'../images/img.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/img.png'} + ${'../images/Image 1.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/Image 1.png'} + ${'/images/img.png'} | ${'bar/baz/foo.php'} | ${'/images/img.png'} + ${'/images/img.png'} | ${'/bar/baz/foo.php'} | ${'/images/img.png'} + ${'../john.md'} | ${'/bar/baz/foo.php'} | ${'/bar/john.md'} + ${'../john.md'} | ${'///bar/baz/foo.php'} | ${'/bar/john.md'} + ${'/images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/images/img.png'} + ${'../images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/img.png'} + ${'../images/Image 1.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/Image%201.png'} + `( + 'converts relative path "$path" with base "$base" to absolute path => "expected"', + ({ path, base, result }) => { + expect(urlUtils.relativePathToAbsolute(path, base)).toBe(result); + }, + ); + }); + describe('isSafeUrl', () => { const absoluteUrls = [ 'http://example.org', @@ -386,6 +481,12 @@ describe('URL utility', () => { expect(urlUtils.queryToObject(searchQuery)).toEqual({ one: '1', two: '2' }); }); + + it('removes undefined values from the search query', () => { + const searchQuery = '?one=1&two=2&three'; + + expect(urlUtils.queryToObject(searchQuery)).toEqual({ one: '1', two: '2' }); + }); }); describe('objectToQuery', () => { |