diff options
Diffstat (limited to 'spec/frontend/lib')
-rw-r--r-- | spec/frontend/lib/dompurify_spec.js | 10 | ||||
-rw-r--r-- | spec/frontend/lib/gfm/index_spec.js | 6 | ||||
-rw-r--r-- | spec/frontend/lib/utils/common_utils_spec.js | 69 | ||||
-rw-r--r-- | spec/frontend/lib/utils/datetime_utility_spec.js | 4 | ||||
-rw-r--r-- | spec/frontend/lib/utils/dom_utils_spec.js | 29 | ||||
-rw-r--r-- | spec/frontend/lib/utils/file_upload_spec.js | 7 | ||||
-rw-r--r-- | spec/frontend/lib/utils/mock_data.js | 42 | ||||
-rw-r--r-- | spec/frontend/lib/utils/navigation_utility_spec.js | 5 | ||||
-rw-r--r-- | spec/frontend/lib/utils/resize_observer_spec.js | 4 | ||||
-rw-r--r-- | spec/frontend/lib/utils/text_markdown_spec.js | 33 | ||||
-rw-r--r-- | spec/frontend/lib/utils/url_utility_spec.js | 53 | ||||
-rw-r--r-- | spec/frontend/lib/utils/users_cache_spec.js | 25 |
12 files changed, 225 insertions, 62 deletions
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js index 47a94a4dcde..34325dad6a1 100644 --- a/spec/frontend/lib/dompurify_spec.js +++ b/spec/frontend/lib/dompurify_spec.js @@ -73,6 +73,16 @@ describe('~/lib/dompurify', () => { expect(sanitize('<p><gl-emoji>💯</gl-emoji></p>')).toBe('<p><gl-emoji>💯</gl-emoji></p>'); }); + it("doesn't allow style tags", () => { + // removes style tags + expect(sanitize('<style>p {width:50%;}</style>')).toBe(''); + expect(sanitize('<style type="text/css">p {width:50%;}</style>')).toBe(''); + // removes mstyle tag (this can removed later by disallowing math tags) + expect(sanitize('<math><mstyle displaystyle="true"></mstyle></math>')).toBe('<math></math>'); + // removes link tag (this is DOMPurify's default behavior) + expect(sanitize('<link rel="stylesheet" href="styles.css">')).toBe(''); + }); + describe.each` type | gon ${'root'} | ${rootGon} diff --git a/spec/frontend/lib/gfm/index_spec.js b/spec/frontend/lib/gfm/index_spec.js index 5c72b5a51a7..c9a480e9943 100644 --- a/spec/frontend/lib/gfm/index_spec.js +++ b/spec/frontend/lib/gfm/index_spec.js @@ -33,14 +33,16 @@ describe('gfm', () => { }); it('returns the result of executing the renderer function', async () => { + const rendered = { value: 'rendered tree' }; + const result = await render({ markdown: '<strong>This is bold text</strong>', renderer: () => { - return 'rendered tree'; + return rendered; }, }); - expect(result).toBe('rendered tree'); + expect(result).toEqual(rendered); }); }); }); diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js index 763a9bd30fe..8e499844406 100644 --- a/spec/frontend/lib/utils/common_utils_spec.js +++ b/spec/frontend/lib/utils/common_utils_spec.js @@ -283,6 +283,75 @@ describe('common_utils', () => { }); }); + describe('insertText', () => { + let textArea; + + beforeAll(() => { + textArea = document.createElement('textarea'); + document.querySelector('body').appendChild(textArea); + textArea.value = 'two'; + textArea.setSelectionRange(0, 0); + textArea.focus(); + }); + + afterAll(() => { + textArea.parentNode.removeChild(textArea); + }); + + describe('using execCommand', () => { + beforeAll(() => { + document.execCommand = jest.fn(() => true); + }); + + it('inserts the text', () => { + commonUtils.insertText(textArea, 'one'); + + expect(document.execCommand).toHaveBeenCalledWith('insertText', false, 'one'); + }); + + it('removes selected text', () => { + textArea.setSelectionRange(0, textArea.value.length); + + commonUtils.insertText(textArea, ''); + + expect(document.execCommand).toHaveBeenCalledWith('delete'); + }); + }); + + describe('using fallback', () => { + beforeEach(() => { + document.execCommand = jest.fn(() => false); + jest.spyOn(textArea, 'dispatchEvent'); + textArea.value = 'two'; + textArea.setSelectionRange(0, 0); + }); + + it('inserts the text', () => { + commonUtils.insertText(textArea, 'one'); + + expect(textArea.value).toBe('onetwo'); + expect(textArea.dispatchEvent).toHaveBeenCalled(); + }); + + it('replaces the selection', () => { + textArea.setSelectionRange(0, textArea.value.length); + + commonUtils.insertText(textArea, 'one'); + + expect(textArea.value).toBe('one'); + expect(textArea.selectionStart).toBe(textArea.value.length); + }); + + it('removes selected text', () => { + textArea.setSelectionRange(0, textArea.value.length); + + commonUtils.insertText(textArea, ''); + + expect(textArea.value).toBe(''); + }); + }); + }); + describe('normalizedHeaders', () => { it('should upperCase all the header keys to keep them consistent', () => { const apiHeaders = { diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js index 7a64b654baa..8d989350173 100644 --- a/spec/frontend/lib/utils/datetime_utility_spec.js +++ b/spec/frontend/lib/utils/datetime_utility_spec.js @@ -308,7 +308,9 @@ describe('datefix', () => { }); describe('parsePikadayDate', () => { - // removed because of https://gitlab.com/gitlab-org/gitlab-foss/issues/39834 + it('should return a UTC date', () => { + expect(datetimeUtility.parsePikadayDate('2020-01-29')).toEqual(new Date(2020, 0, 29)); + }); }); describe('pikadayToString', () => { diff --git a/spec/frontend/lib/utils/dom_utils_spec.js b/spec/frontend/lib/utils/dom_utils_spec.js index 2f240f25d2a..88dac449527 100644 --- a/spec/frontend/lib/utils/dom_utils_spec.js +++ b/spec/frontend/lib/utils/dom_utils_spec.js @@ -1,3 +1,4 @@ +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { addClassIfElementExists, canScrollUp, @@ -6,6 +7,7 @@ import { isElementVisible, isElementHidden, getParents, + getParentByTagName, setAttributes, } from '~/lib/utils/dom_utils'; @@ -23,10 +25,14 @@ describe('DOM Utils', () => { let parentElement; beforeEach(() => { - setFixtures(fixture); + setHTMLFixture(fixture); parentElement = document.querySelector('.parent'); }); + afterEach(() => { + resetHTMLFixture(); + }); + it('adds class if element exists', () => { const childElement = parentElement.querySelector('.child'); @@ -126,10 +132,14 @@ describe('DOM Utils', () => { let element; beforeEach(() => { - setFixtures('<div data-foo-bar data-baz data-qux="">'); + setHTMLFixture('<div data-foo-bar data-baz data-qux="">'); element = document.querySelector('[data-foo-bar]'); }); + afterEach(() => { + resetHTMLFixture(); + }); + it('throws if not given an element', () => { expect(() => parseBooleanDataAttributes(null, ['baz'])).toThrow(); }); @@ -210,6 +220,21 @@ describe('DOM Utils', () => { }); }); + describe('getParentByTagName', () => { + const el = document.createElement('div'); + el.innerHTML = '<p><span><strong><mark>hello world'; + + it.each` + tagName | parent + ${'strong'} | ${el.querySelector('strong')} + ${'span'} | ${el.querySelector('span')} + ${'p'} | ${el.querySelector('p')} + ${'pre'} | ${undefined} + `('gets a parent by tag name', ({ tagName, parent }) => { + expect(getParentByTagName(el.querySelector('mark'), tagName)).toBe(parent); + }); + }); + describe('setAttributes', () => { it('sets multiple attribues on element', () => { const div = document.createElement('div'); diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js index ff11107ea60..f63af2fe0a4 100644 --- a/spec/frontend/lib/utils/file_upload_spec.js +++ b/spec/frontend/lib/utils/file_upload_spec.js @@ -1,8 +1,9 @@ +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import fileUpload, { getFilename, validateImageName } from '~/lib/utils/file_upload'; describe('File upload', () => { beforeEach(() => { - setFixtures(` + setHTMLFixture(` <form> <button class="js-button" type="button">Click me!</button> <input type="text" class="js-input" /> @@ -11,6 +12,10 @@ describe('File upload', () => { `); }); + afterEach(() => { + resetHTMLFixture(); + }); + describe('when there is a matching button and input', () => { beforeEach(() => { fileUpload('.js-button', '.js-input'); diff --git a/spec/frontend/lib/utils/mock_data.js b/spec/frontend/lib/utils/mock_data.js index df1f79529e7..49a2af8b307 100644 --- a/spec/frontend/lib/utils/mock_data.js +++ b/spec/frontend/lib/utils/mock_data.js @@ -3,3 +3,45 @@ export const faviconDataUrl = export const overlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA85JREFUWAntVllIVGEUPv/9b46O41KplYN7PeRkti8TjQlhCUGh3MmeQugpIsGKAi2soIcIooiohxYKK2daqDAlIpIiWwxtQaJcaHE0d5tMrbn37z9XRqfR0TvVW56Hudf//uec72zfEWBCJjIwkYGJDPzvGSD/KgExN3Oi2Q+2DJgSDYQEMwItVGH1iZGmJw/Si1y+/PwVAMYYib22MYc/8hVQFgKDEfYoId0KYzagAQebsos/ewMZoeB9wdffcTYpQSaCTWHKoqSQaDk7zkIt0+aCUR8BelEHrf3dUNv9AcqbnsHtT5UKB/hTASh0SLYjnjb/CIDRJi0XiFAaJOpCD8zLpdb4NB66b1OfelthX815dtdRRfiti2aAXLvVLiMQ6olGyztGDkSo4JGGXk8/QFdGpYzpHG2GBQTDhtgVhPEaVbbVpvI6GJz22rv4TcAfrYI1x7Rj5MWWAppomKFVVb2302SFzUkZHAbkG+0b1+Gh77yNYjrmqnWTrLBLRxdvBWv8qlFujH/kYjJYyvLkj71t78zAUvzMAMnHhpN4zf9UREJhd8omyssxu1IgazQDwDnHUcNuH6vhPIE1fmuBzHt74Hn7W89jWGtcAjoaIDOFrdcMYJBkgOCoaRF0Lj0oglddDbCj6tRvKjphEpgjkzEQs2YAKsNxMzjn3nKurhzK+Ly7xe28ua8TwgMMcHJZnvvT0BPtEEKM4tDJ+C8GvIIk4ylINIXVZ0EUKJxYuh3mhCeokbudl6TtVc88dfBdLwbyaWB6zQCYQJpBYSrDGQxBQ/ZWRM2B+VNmQnVnHWx7elyNuL2/R336co7KyJR8CL9oLgEuFlREevWUkEl6uGwpVEG4FBm0OEf9N10NMgPlvWYAuNVwsWDKvcUNYsHUWTCZ13ysyFEXe6TO6aC8CUr9IiK+A05TQrc8yjwmxARHeeMAPlfQJw+AQRwu0YhL/GDXi9NwufG+S8dYkuYMqIb4SsWthotlNMOUCOM6r+G9cqXxPmd1dqrBav/o1zJy2l5/NUjJA/VORwYuFnOUaTQcPs9wMqwV++Xv8oADxKAcZ8nLPr8AoGW+xR6HSqYk3GodAz2QNj0V+Gr26dT9ASNH5239Pf0gktVNWZca8ZvfAFBprWS6hSu1pqt++Y0PD+WIwDAhIWQGtzvSHDbcodfFUFB9hg1Gjs5LXqIdFL+acFBl+FddqYwdxsWC3I70OvgfUaA65zhq2O2c8VxYcyIGFTVlXegYtvCXANCQZJMobjVcLMjtSK/IcEgyOOe8Ve5w7ryKDefp2P3+C/5ohv8HZmVLAAAAAElFTkSuQmCC'; + +const absoluteUrls = [ + 'http://example.org', + 'http://example.org:8080', + 'https://example.org', + 'https://example.org:8080', + 'https://192.168.1.1', +]; + +const rootRelativeUrls = ['/relative/link']; + +const relativeUrls = ['./relative/link', '../relative/link']; + +const urlsWithoutHost = ['http://', 'https://', 'https:https:https:']; + +/* eslint-disable no-script-url */ +const nonHttpUrls = [ + 'javascript:', + 'javascript:alert("XSS")', + 'jav\tascript:alert("XSS");', + '  javascript:alert("XSS");', + 'ftp://192.168.1.1', + 'file:///', + 'file:///etc/hosts', +]; +/* eslint-enable no-script-url */ + +// javascript:alert('XSS') +const encodedJavaScriptUrls = [ + 'javascript:alert('XSS')', + 'javascript:alert('XSS')', + 'javascript:alert('XSS')', + '\\u006A\\u0061\\u0076\\u0061\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003A\\u0061\\u006C\\u0065\\u0072\\u0074\\u0028\\u0027\\u0058\\u0053\\u0053\\u0027\\u0029', +]; + +export const safeUrls = [...absoluteUrls, ...rootRelativeUrls]; +export const unsafeUrls = [ + ...relativeUrls, + ...urlsWithoutHost, + ...nonHttpUrls, + ...encodedJavaScriptUrls, +]; diff --git a/spec/frontend/lib/utils/navigation_utility_spec.js b/spec/frontend/lib/utils/navigation_utility_spec.js index 6a880a0f354..632a8904578 100644 --- a/spec/frontend/lib/utils/navigation_utility_spec.js +++ b/spec/frontend/lib/utils/navigation_utility_spec.js @@ -1,3 +1,4 @@ +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import findAndFollowLink from '~/lib/utils/navigation_utility'; import * as navigationUtils from '~/lib/utils/navigation_utility'; import { visitUrl } from '~/lib/utils/url_utility'; @@ -8,11 +9,13 @@ describe('findAndFollowLink', () => { it('visits a link when the selector exists', () => { const href = '/some/path'; - setFixtures(`<a class="my-shortcut" href="${href}">link</a>`); + setHTMLFixture(`<a class="my-shortcut" href="${href}">link</a>`); findAndFollowLink('.my-shortcut'); expect(visitUrl).toHaveBeenCalledWith(href); + + resetHTMLFixture(); }); it('does not throw an exception when the selector does not exist', () => { diff --git a/spec/frontend/lib/utils/resize_observer_spec.js b/spec/frontend/lib/utils/resize_observer_spec.js index 6560562f204..c88ba73ebc6 100644 --- a/spec/frontend/lib/utils/resize_observer_spec.js +++ b/spec/frontend/lib/utils/resize_observer_spec.js @@ -1,3 +1,4 @@ +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { contentTop } from '~/lib/utils/common_utils'; import { scrollToTargetOnResize } from '~/lib/utils/resize_observer'; @@ -19,7 +20,7 @@ describe('ResizeObserver Utility', () => { jest.spyOn(document.documentElement, 'scrollTo'); - setFixtures(`<div id="content-body"><div id="note_1234">note to scroll to</div></div>`); + setHTMLFixture(`<div id="content-body"><div id="note_1234">note to scroll to</div></div>`); const target = document.querySelector('#note_1234'); @@ -28,6 +29,7 @@ describe('ResizeObserver Utility', () => { afterEach(() => { contentTop.mockReset(); + resetHTMLFixture(); }); describe('Observer behavior', () => { diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 103305f0797..d1bca3c73b6 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -1,5 +1,10 @@ import $ from 'jquery'; -import { insertMarkdownText, keypressNoteText } from '~/lib/utils/text_markdown'; +import { + insertMarkdownText, + keypressNoteText, + compositionStartNoteText, + compositionEndNoteText, +} from '~/lib/utils/text_markdown'; import '~/lib/utils/jquery_at_who'; describe('init markdown', () => { @@ -9,6 +14,9 @@ describe('init markdown', () => { textArea = document.createElement('textarea'); document.querySelector('body').appendChild(textArea); textArea.focus(); + + // needed for the underlying insertText to work + document.execCommand = jest.fn(() => false); }); afterAll(() => { @@ -172,7 +180,9 @@ describe('init markdown', () => { const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' }); beforeEach(() => { - gon.features = { markdownContinueLists: true }; + textArea.addEventListener('keydown', keypressNoteText); + textArea.addEventListener('compositionstart', compositionStartNoteText); + textArea.addEventListener('compositionend', compositionEndNoteText); }); it.each` @@ -203,7 +213,6 @@ describe('init markdown', () => { textArea.value = text; textArea.setSelectionRange(text.length, text.length); - textArea.addEventListener('keydown', keypressNoteText); textArea.dispatchEvent(enterEvent); expect(textArea.value).toEqual(expected); @@ -231,7 +240,6 @@ describe('init markdown', () => { textArea.value = text; textArea.setSelectionRange(text.length, text.length); - textArea.addEventListener('keydown', keypressNoteText); textArea.dispatchEvent(enterEvent); expect(textArea.value.substr(0, textArea.selectionStart)).toEqual(expected); @@ -251,7 +259,6 @@ describe('init markdown', () => { textArea.value = text; textArea.setSelectionRange(text.length, text.length); - textArea.addEventListener('keydown', keypressNoteText); textArea.dispatchEvent(enterEvent); expect(textArea.value).toEqual(expected); @@ -267,23 +274,25 @@ describe('init markdown', () => { textArea.value = text; textArea.setSelectionRange(add_at, add_at); - textArea.addEventListener('keydown', keypressNoteText); textArea.dispatchEvent(enterEvent); expect(textArea.value).toEqual(expected); }, ); - it('does nothing if feature flag disabled', () => { - gon.features = { markdownContinueLists: false }; - - const text = '- item'; - const expected = '- item'; + it('does not duplicate a line item for IME characters', () => { + const text = '- 日本語'; + const expected = '- 日本語\n- '; + textArea.dispatchEvent(new CompositionEvent('compositionstart')); textArea.value = text; + + // Press enter to end composition + textArea.dispatchEvent(enterEvent); + textArea.dispatchEvent(new CompositionEvent('compositionend')); textArea.setSelectionRange(text.length, text.length); - textArea.addEventListener('keydown', keypressNoteText); + // Press enter to make new line textArea.dispatchEvent(enterEvent); expect(textArea.value).toEqual(expected); diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 7608cff4c9e..81cf4bd293b 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -1,6 +1,7 @@ import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; import * as urlUtils from '~/lib/utils/url_utility'; +import { safeUrls, unsafeUrls } from './mock_data'; const shas = { valid: [ @@ -575,48 +576,6 @@ describe('URL utility', () => { }); describe('isSafeUrl', () => { - const absoluteUrls = [ - 'http://example.org', - 'http://example.org:8080', - 'https://example.org', - 'https://example.org:8080', - 'https://192.168.1.1', - ]; - - const rootRelativeUrls = ['/relative/link']; - - const relativeUrls = ['./relative/link', '../relative/link']; - - const urlsWithoutHost = ['http://', 'https://', 'https:https:https:']; - - /* eslint-disable no-script-url */ - const nonHttpUrls = [ - 'javascript:', - 'javascript:alert("XSS")', - 'jav\tascript:alert("XSS");', - '  javascript:alert("XSS");', - 'ftp://192.168.1.1', - 'file:///', - 'file:///etc/hosts', - ]; - /* eslint-enable no-script-url */ - - // javascript:alert('XSS') - const encodedJavaScriptUrls = [ - 'javascript:alert('XSS')', - 'javascript:alert('XSS')', - 'javascript:alert('XSS')', - '\\u006A\\u0061\\u0076\\u0061\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003A\\u0061\\u006C\\u0065\\u0072\\u0074\\u0028\\u0027\\u0058\\u0053\\u0053\\u0027\\u0029', - ]; - - const safeUrls = [...absoluteUrls, ...rootRelativeUrls]; - const unsafeUrls = [ - ...relativeUrls, - ...urlsWithoutHost, - ...nonHttpUrls, - ...encodedJavaScriptUrls, - ]; - describe('with URL constructor support', () => { it.each(safeUrls)('returns true for %s', (url) => { expect(urlUtils.isSafeURL(url)).toBe(true); @@ -628,6 +587,16 @@ describe('URL utility', () => { }); }); + describe('sanitizeUrl', () => { + it.each(safeUrls)('returns the url for %s', (url) => { + expect(urlUtils.sanitizeUrl(url)).toBe(url); + }); + + it.each(unsafeUrls)('returns `about:blank` for %s', (url) => { + expect(urlUtils.sanitizeUrl(url)).toBe('about:blank'); + }); + }); + describe('getNormalizedURL', () => { it.each` url | base | result diff --git a/spec/frontend/lib/utils/users_cache_spec.js b/spec/frontend/lib/utils/users_cache_spec.js index 30bdddd8e73..d35ba20f570 100644 --- a/spec/frontend/lib/utils/users_cache_spec.js +++ b/spec/frontend/lib/utils/users_cache_spec.js @@ -228,4 +228,29 @@ describe('UsersCache', () => { expect(userStatus).toBe(dummyUserStatus); }); }); + + describe('updateById', () => { + describe('when the user is not cached', () => { + it('does nothing and returns undefined', () => { + expect(UsersCache.updateById(dummyUserId, { name: 'root' })).toBe(undefined); + expect(UsersCache.internalStorage).toStrictEqual({}); + }); + }); + + describe('when the user is cached', () => { + const updatedName = 'has two farms'; + beforeEach(() => { + UsersCache.internalStorage[dummyUserId] = dummyUser; + }); + + it('updates the user only with the new data', async () => { + UsersCache.updateById(dummyUserId, { name: updatedName }); + + expect(await UsersCache.retrieveById(dummyUserId)).toStrictEqual({ + username: dummyUser.username, + name: updatedName, + }); + }); + }); + }); }); |