diff options
Diffstat (limited to 'spec/frontend/lib')
-rw-r--r-- | spec/frontend/lib/dompurify_spec.js | 16 | ||||
-rw-r--r-- | spec/frontend/lib/graphql_spec.js | 54 | ||||
-rw-r--r-- | spec/frontend/lib/utils/common_utils_spec.js | 146 | ||||
-rw-r--r-- | spec/frontend/lib/utils/datetime/timeago_utility_spec.js | 103 | ||||
-rw-r--r-- | spec/frontend/lib/utils/datetime_utility_spec.js | 40 | ||||
-rw-r--r-- | spec/frontend/lib/utils/finite_state_machine_spec.js | 293 | ||||
-rw-r--r-- | spec/frontend/lib/utils/text_markdown_spec.js | 2 | ||||
-rw-r--r-- | spec/frontend/lib/utils/url_utility_spec.js | 115 |
8 files changed, 626 insertions, 143 deletions
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js index a01f86678e9..fa8dbb12a08 100644 --- a/spec/frontend/lib/dompurify_spec.js +++ b/spec/frontend/lib/dompurify_spec.js @@ -30,6 +30,9 @@ const unsafeUrls = [ `https://evil.url/${absoluteGon.sprite_file_icons}`, ]; +const forbiddenDataAttrs = ['data-remote', 'data-url', 'data-type', 'data-method']; +const acceptedDataAttrs = ['data-random', 'data-custom']; + describe('~/lib/dompurify', () => { let originalGon; @@ -95,4 +98,17 @@ describe('~/lib/dompurify', () => { expect(sanitize(htmlXlink)).toBe(expectedSanitized); }); }); + + describe('handles data attributes correctly', () => { + it.each(forbiddenDataAttrs)('removes %s attributes', (attr) => { + const htmlHref = `<a ${attr}="true">hello</a>`; + expect(sanitize(htmlHref)).toBe('<a>hello</a>'); + }); + + it.each(acceptedDataAttrs)('does not remove %s attributes', (attr) => { + const attrWithValue = `${attr}="true"`; + const htmlHref = `<a ${attrWithValue}>hello</a>`; + expect(sanitize(htmlHref)).toBe(`<a ${attrWithValue}>hello</a>`); + }); + }); }); diff --git a/spec/frontend/lib/graphql_spec.js b/spec/frontend/lib/graphql_spec.js new file mode 100644 index 00000000000..a39ce2ffd99 --- /dev/null +++ b/spec/frontend/lib/graphql_spec.js @@ -0,0 +1,54 @@ +import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; +import { stripWhitespaceFromQuery } from '~/lib/graphql'; +import { queryToObject } from '~/lib/utils/url_utility'; + +describe('stripWhitespaceFromQuery', () => { + const operationName = 'getPipelineDetails'; + const variables = `{ + projectPath: 'root/abcd-dag', + iid: '44' + }`; + + const testQuery = getPipelineDetails.loc.source.body; + const defaultPath = '/api/graphql'; + const encodedVariables = encodeURIComponent(variables); + + it('shortens the query argument by replacing multiple spaces and newlines with a single space', () => { + const testString = `${defaultPath}?query=${encodeURIComponent(testQuery)}`; + expect(testString.length > stripWhitespaceFromQuery(testString, defaultPath).length).toBe(true); + }); + + it('does not contract a single space', () => { + const simpleSingleString = `${defaultPath}?query=${encodeURIComponent('fragment Nonsense')}`; + expect(stripWhitespaceFromQuery(simpleSingleString, defaultPath)).toEqual(simpleSingleString); + }); + + it('works with a non-default path', () => { + const newPath = 'another/graphql/path'; + const newPathSingleString = `${newPath}?query=${encodeURIComponent('fragment Nonsense')}`; + expect(stripWhitespaceFromQuery(newPathSingleString, newPath)).toEqual(newPathSingleString); + }); + + it('does not alter other arguments', () => { + const bareParams = `?query=${encodeURIComponent( + testQuery, + )}&operationName=${operationName}&variables=${encodedVariables}`; + const testLongString = `${defaultPath}${bareParams}`; + + const processed = stripWhitespaceFromQuery(testLongString, defaultPath); + const decoded = decodeURIComponent(processed); + const params = queryToObject(decoded); + + expect(params.operationName).toBe(operationName); + expect(params.variables).toBe(variables); + }); + + it('works when there are no query params', () => { + expect(stripWhitespaceFromQuery(defaultPath, defaultPath)).toEqual(defaultPath); + }); + + it('works when the params do not include a query', () => { + const paramsWithoutQuery = `${defaultPath}&variables=${encodedVariables}`; + expect(stripWhitespaceFromQuery(paramsWithoutQuery, defaultPath)).toEqual(paramsWithoutQuery); + }); +}); diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js index e03d1ef7295..f5a74ee7f09 100644 --- a/spec/frontend/lib/utils/common_utils_spec.js +++ b/spec/frontend/lib/utils/common_utils_spec.js @@ -1,6 +1,56 @@ import * as commonUtils from '~/lib/utils/common_utils'; describe('common_utils', () => { + describe('getPagePath', () => { + const { getPagePath } = commonUtils; + + let originalBody; + + beforeEach(() => { + originalBody = document.body; + document.body = document.createElement('body'); + }); + + afterEach(() => { + document.body = originalBody; + }); + + it('returns an empty path if none is defined', () => { + expect(getPagePath()).toBe(''); + expect(getPagePath(0)).toBe(''); + }); + + describe('returns a path', () => { + const mockSection = 'my_section'; + const mockSubSection = 'my_sub_section'; + const mockPage = 'my_page'; + + it('returns a page', () => { + document.body.dataset.page = mockPage; + + expect(getPagePath()).toBe(mockPage); + expect(getPagePath(0)).toBe(mockPage); + }); + + it('returns a section and page', () => { + document.body.dataset.page = `${mockSection}:${mockPage}`; + + expect(getPagePath()).toBe(mockSection); + expect(getPagePath(0)).toBe(mockSection); + expect(getPagePath(1)).toBe(mockPage); + }); + + it('returns a section and subsection', () => { + document.body.dataset.page = `${mockSection}:${mockSubSection}:${mockPage}`; + + expect(getPagePath()).toBe(mockSection); + expect(getPagePath(0)).toBe(mockSection); + expect(getPagePath(1)).toBe(mockSubSection); + expect(getPagePath(2)).toBe(mockPage); + }); + }); + }); + describe('parseUrl', () => { it('returns an anchor tag with url', () => { expect(commonUtils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url'); @@ -26,42 +76,6 @@ describe('common_utils', () => { }); }); - describe('urlParamsToArray', () => { - it('returns empty array for empty querystring', () => { - expect(commonUtils.urlParamsToArray('')).toEqual([]); - }); - - it('should decode params', () => { - expect(commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0]).toBe('label_name[]=test'); - }); - - it('should remove the question mark from the search params', () => { - const paramsArray = commonUtils.urlParamsToArray('?test=thing'); - - expect(paramsArray[0][0]).not.toBe('?'); - }); - }); - - describe('urlParamsToObject', () => { - it('parses path for label with trailing +', () => { - expect(commonUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({ - label_name: ['label+'], - }); - }); - - it('parses path for milestone with trailing +', () => { - expect(commonUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({ - milestone_title: 'A+', - }); - }); - - it('parses path for search terms with spaces', () => { - expect(commonUtils.urlParamsToObject('search=two+words', {})).toEqual({ - search: 'two words', - }); - }); - }); - describe('handleLocationHash', () => { beforeEach(() => { jest.spyOn(window.document, 'getElementById'); @@ -175,33 +189,6 @@ describe('common_utils', () => { }); }); - describe('parseQueryStringIntoObject', () => { - it('should return object with query parameters', () => { - expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ - scope: 'all', - page: '2', - }); - - expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' }); - expect(commonUtils.parseQueryStringIntoObject()).toEqual({}); - }); - }); - - describe('objectToQueryString', () => { - it('returns empty string when `param` is undefined, null or empty string', () => { - expect(commonUtils.objectToQueryString()).toBe(''); - expect(commonUtils.objectToQueryString('')).toBe(''); - }); - - it('returns query string with values of `params`', () => { - const singleQueryParams = { foo: true }; - const multipleQueryParams = { foo: true, bar: true }; - - expect(commonUtils.objectToQueryString(singleQueryParams)).toBe('foo=true'); - expect(commonUtils.objectToQueryString(multipleQueryParams)).toBe('foo=true&bar=true'); - }); - }); - describe('buildUrlWithCurrentLocation', () => { it('should build an url with current location and given parameters', () => { expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); @@ -310,39 +297,6 @@ describe('common_utils', () => { }); }); - describe('getParameterByName', () => { - beforeEach(() => { - window.history.pushState({}, null, '?scope=all&p=2'); - }); - - afterEach(() => { - window.history.replaceState({}, null, null); - }); - - it('should return valid parameter', () => { - const value = commonUtils.getParameterByName('scope'); - - expect(commonUtils.getParameterByName('p')).toEqual('2'); - expect(value).toBe('all'); - }); - - it('should return invalid parameter', () => { - const value = commonUtils.getParameterByName('fakeParameter'); - - expect(value).toBe(null); - }); - - it('should return valid paramentes if URL is provided', () => { - let value = commonUtils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar'); - - expect(value).toBe('bar'); - - value = commonUtils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu'); - - expect(value).toBe('canchu'); - }); - }); - describe('normalizedHeaders', () => { it('should upperCase all the header keys to keep them consistent', () => { const apiHeaders = { diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js new file mode 100644 index 00000000000..2314ec678d3 --- /dev/null +++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js @@ -0,0 +1,103 @@ +import { getTimeago, localTimeAgo, timeFor } 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(() => { + window.gon = { time_display_relative: true }; + }); + + it.each([ + [new Date().toISOString(), 'just now'], + [new Date().getTime(), 'just now'], + [new Date(), 'just now'], + [null, 'just now'], + ])('formats date `%p` as `%p`', (date, result) => { + expect(getTimeago().format(date)).toEqual(result); + }); + }); + + describe('with User Setting timeDisplayRelative: false', () => { + beforeEach(() => { + window.gon = { time_display_relative: false }; + }); + + it.each([ + [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) => { + expect(getTimeago().format(date)).toEqual(result); + }); + }); + }); + + describe('timeFor', () => { + it('returns localize `past due` when in past', () => { + const date = new Date(); + date.setFullYear(date.getFullYear() - 1); + + expect(timeFor(date)).toBe(s__('Timeago|Past due')); + }); + + it('returns localized remaining time when in the future', () => { + const date = new Date(); + date.setFullYear(date.getFullYear() + 1); + + // Add a day to prevent a transient error. If date is even 1 second + // short of a full year, timeFor will return '11 months remaining' + date.setDate(date.getDate() + 1); + + expect(timeFor(date)).toBe(s__('Timeago|1 year remaining')); + }); + }); + + describe('localTimeAgo', () => { + beforeEach(() => { + document.body.innerHTML = + '<time title="some time" datetime="2020-02-18T22:22:32Z">1 hour ago</time>'; + }); + + describe.each` + timeDisplayRelative | text + ${true} | ${'4 months ago'} + ${false} | ${'Feb 18, 2020, 10:22 PM'} + `( + `With User Setting timeDisplayRelative: $timeDisplayRelative`, + ({ timeDisplayRelative, text }) => { + it.each` + updateTooltip | title + ${false} | ${'some time'} + ${true} | ${'Feb 18, 2020 10:22pm UTC'} + `( + `has content: '${text}' and tooltip: '$title' with updateTooltip = $updateTooltip`, + ({ updateTooltip, title }) => { + window.gon = { time_display_relative: timeDisplayRelative }; + + const element = document.querySelector('time'); + localTimeAgo([element], updateTooltip); + + jest.runAllTimers(); + + expect(element.getAttribute('title')).toBe(title); + expect(element.innerText).toBe(text); + }, + ); + }, + ); + }); +}); diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js index df0ccb19cb7..f6ad41d5478 100644 --- a/spec/frontend/lib/utils/datetime_utility_spec.js +++ b/spec/frontend/lib/utils/datetime_utility_spec.js @@ -1,30 +1,9 @@ -import $ from 'jquery'; import timezoneMock from 'timezone-mock'; import * as datetimeUtility from '~/lib/utils/datetime_utility'; import { __, s__ } from '~/locale'; import '~/commons/bootstrap'; describe('Date time utils', () => { - describe('timeFor', () => { - it('returns localize `past due` when in past', () => { - const date = new Date(); - date.setFullYear(date.getFullYear() - 1); - - expect(datetimeUtility.timeFor(date)).toBe(s__('Timeago|Past due')); - }); - - it('returns localized remaining time when in the future', () => { - const date = new Date(); - date.setFullYear(date.getFullYear() + 1); - - // Add a day to prevent a transient error. If date is even 1 second - // short of a full year, timeFor will return '11 months remaining' - date.setDate(date.getDate() + 1); - - expect(datetimeUtility.timeFor(date)).toBe(s__('Timeago|1 year remaining')); - }); - }); - describe('get localized day name', () => { it('should return Sunday', () => { const day = datetimeUtility.getDayName(new Date('07/17/2016')); @@ -870,25 +849,6 @@ describe('approximateDuration', () => { }); }); -describe('localTimeAgo', () => { - beforeEach(() => { - document.body.innerHTML = `<time title="some time" datetime="2020-02-18T22:22:32Z">1 hour ago</time>`; - }); - - it.each` - timeagoArg | title - ${false} | ${'some time'} - ${true} | ${'Feb 18, 2020 10:22pm UTC'} - `('converts $seconds seconds to $approximation', ({ timeagoArg, title }) => { - const element = document.querySelector('time'); - datetimeUtility.localTimeAgo($(element), timeagoArg); - - jest.runAllTimers(); - - expect(element.getAttribute('title')).toBe(title); - }); -}); - describe('differenceInSeconds', () => { const startDateTime = new Date('2019-07-17T00:00:00.000Z'); diff --git a/spec/frontend/lib/utils/finite_state_machine_spec.js b/spec/frontend/lib/utils/finite_state_machine_spec.js new file mode 100644 index 00000000000..441dd24c758 --- /dev/null +++ b/spec/frontend/lib/utils/finite_state_machine_spec.js @@ -0,0 +1,293 @@ +import { machine, transition } from '~/lib/utils/finite_state_machine'; + +describe('Finite State Machine', () => { + const STATE_IDLE = 'idle'; + const STATE_LOADING = 'loading'; + const STATE_ERRORED = 'errored'; + + const TRANSITION_START_LOAD = 'START_LOAD'; + const TRANSITION_LOAD_ERROR = 'LOAD_ERROR'; + const TRANSITION_LOAD_SUCCESS = 'LOAD_SUCCESS'; + const TRANSITION_ACKNOWLEDGE_ERROR = 'ACKNOWLEDGE_ERROR'; + + const definition = { + initial: STATE_IDLE, + states: { + [STATE_IDLE]: { + on: { + [TRANSITION_START_LOAD]: STATE_LOADING, + }, + }, + [STATE_LOADING]: { + on: { + [TRANSITION_LOAD_ERROR]: STATE_ERRORED, + [TRANSITION_LOAD_SUCCESS]: STATE_IDLE, + }, + }, + [STATE_ERRORED]: { + on: { + [TRANSITION_ACKNOWLEDGE_ERROR]: STATE_IDLE, + [TRANSITION_START_LOAD]: STATE_LOADING, + }, + }, + }, + }; + + describe('machine', () => { + const STATE_IMPOSSIBLE = 'impossible'; + const badDefinition = { + init: definition.initial, + badKeyShouldBeStates: definition.states, + }; + const unstartableDefinition = { + initial: STATE_IMPOSSIBLE, + states: definition.states, + }; + let liveMachine; + + beforeEach(() => { + liveMachine = machine(definition); + }); + + it('throws an error if the machine definition is invalid', () => { + expect(() => machine(badDefinition)).toThrowError( + 'A state machine must have an initial state (`.initial`) and a dictionary of possible states (`.states`)', + ); + }); + + it('throws an error if the initial state is invalid', () => { + expect(() => machine(unstartableDefinition)).toThrowError( + `Cannot initialize the state machine to state '${STATE_IMPOSSIBLE}'. Is that one of the machine's defined states?`, + ); + }); + + it.each` + partOfMachine | equals | description | eqDescription + ${'keys'} | ${['is', 'send', 'value', 'states']} | ${'keys'} | ${'the correct array'} + ${'is'} | ${expect.any(Function)} | ${'`is` property'} | ${'a function'} + ${'send'} | ${expect.any(Function)} | ${'`send` property'} | ${'a function'} + ${'value'} | ${definition.initial} | ${'`value` property'} | ${'the same as the `initial` value of the machine definition'} + ${'states'} | ${definition.states} | ${'`states` property'} | ${'the same as the `states` value of the machine definition'} + `("The machine's $description should be $eqDescription", ({ partOfMachine, equals }) => { + const test = partOfMachine === 'keys' ? Object.keys(liveMachine) : liveMachine[partOfMachine]; + + expect(test).toEqual(equals); + }); + + it.each` + initialState | transitionEvent | expectedState + ${definition.initial} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} + ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED} + ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE} + ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} + ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE} + `( + 'properly steps from $initialState to $expectedState when the event "$transitionEvent" is sent', + ({ initialState, transitionEvent, expectedState }) => { + liveMachine.value = initialState; + + liveMachine.send(transitionEvent); + + expect(liveMachine.is(expectedState)).toBe(true); + expect(liveMachine.value).toBe(expectedState); + }, + ); + + it.each` + initialState | transitionEvent + ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR} + ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS} + ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR} + ${STATE_IDLE} | ${'RANDOM_FOO'} + ${STATE_LOADING} | ${TRANSITION_START_LOAD} + ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR} + ${STATE_LOADING} | ${'RANDOM_FOO'} + ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR} + ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS} + ${STATE_ERRORED} | ${'RANDOM_FOO'} + `( + `does not perform any transition if the machine can't move from "$initialState" using the "$transitionEvent" event`, + ({ initialState, transitionEvent }) => { + liveMachine.value = initialState; + + liveMachine.send(transitionEvent); + + expect(liveMachine.is(initialState)).toBe(true); + expect(liveMachine.value).toBe(initialState); + }, + ); + + describe('send', () => { + it.each` + startState | transitionEvent | result + ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} + ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE} + ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED} + ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE} + ${STATE_ERRORED} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} + `( + 'successfully transitions to $result from $startState when the transition $transitionEvent is received', + ({ startState, transitionEvent, result }) => { + liveMachine.value = startState; + + expect(liveMachine.send(transitionEvent)).toEqual(result); + }, + ); + + it.each` + startState | transitionEvent + ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR} + ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS} + ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR} + ${STATE_IDLE} | ${'RANDOM_FOO'} + ${STATE_LOADING} | ${TRANSITION_START_LOAD} + ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR} + ${STATE_LOADING} | ${'RANDOM_FOO'} + ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR} + ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS} + ${STATE_ERRORED} | ${'RANDOM_FOO'} + `( + 'remains as $startState if an undefined transition ($transitionEvent) is received', + ({ startState, transitionEvent }) => { + liveMachine.value = startState; + + expect(liveMachine.send(transitionEvent)).toEqual(startState); + }, + ); + + describe('detached', () => { + it.each` + startState | transitionEvent | result + ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} + ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE} + ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED} + ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE} + ${STATE_ERRORED} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} + `( + 'successfully transitions to $result from $startState when the transition $transitionEvent is received outside the context of the machine', + ({ startState, transitionEvent, result }) => { + const liveSend = machine({ + ...definition, + initial: startState, + }).send; + + expect(liveSend(transitionEvent)).toEqual(result); + }, + ); + + it.each` + startState | transitionEvent + ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR} + ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS} + ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR} + ${STATE_IDLE} | ${'RANDOM_FOO'} + ${STATE_LOADING} | ${TRANSITION_START_LOAD} + ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR} + ${STATE_LOADING} | ${'RANDOM_FOO'} + ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR} + ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS} + ${STATE_ERRORED} | ${'RANDOM_FOO'} + `( + 'remains as $startState if an undefined transition ($transitionEvent) is received', + ({ startState, transitionEvent }) => { + const liveSend = machine({ + ...definition, + initial: startState, + }).send; + + expect(liveSend(transitionEvent)).toEqual(startState); + }, + ); + }); + }); + + describe('is', () => { + it.each` + bool | test | actual + ${true} | ${STATE_IDLE} | ${STATE_IDLE} + ${false} | ${STATE_LOADING} | ${STATE_IDLE} + ${false} | ${STATE_ERRORED} | ${STATE_IDLE} + ${true} | ${STATE_LOADING} | ${STATE_LOADING} + ${false} | ${STATE_IDLE} | ${STATE_LOADING} + ${false} | ${STATE_ERRORED} | ${STATE_LOADING} + ${true} | ${STATE_ERRORED} | ${STATE_ERRORED} + ${false} | ${STATE_IDLE} | ${STATE_ERRORED} + ${false} | ${STATE_LOADING} | ${STATE_ERRORED} + `( + 'returns "$bool" for "$test" when the current state is "$actual"', + ({ bool, test, actual }) => { + liveMachine = machine({ + ...definition, + initial: actual, + }); + + expect(liveMachine.is(test)).toEqual(bool); + }, + ); + + describe('detached', () => { + it.each` + bool | test | actual + ${true} | ${STATE_IDLE} | ${STATE_IDLE} + ${false} | ${STATE_LOADING} | ${STATE_IDLE} + ${false} | ${STATE_ERRORED} | ${STATE_IDLE} + ${true} | ${STATE_LOADING} | ${STATE_LOADING} + ${false} | ${STATE_IDLE} | ${STATE_LOADING} + ${false} | ${STATE_ERRORED} | ${STATE_LOADING} + ${true} | ${STATE_ERRORED} | ${STATE_ERRORED} + ${false} | ${STATE_IDLE} | ${STATE_ERRORED} + ${false} | ${STATE_LOADING} | ${STATE_ERRORED} + `( + 'returns "$bool" for "$test" when the current state is "$actual"', + ({ bool, test, actual }) => { + const liveIs = machine({ + ...definition, + initial: actual, + }).is; + + expect(liveIs(test)).toEqual(bool); + }, + ); + }); + }); + }); + + describe('transition', () => { + it.each` + startState | transitionEvent | result + ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} + ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE} + ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED} + ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE} + ${STATE_ERRORED} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} + `( + 'successfully transitions to $result from $startState when the transition $transitionEvent is received', + ({ startState, transitionEvent, result }) => { + expect(transition(definition, startState, transitionEvent)).toEqual(result); + }, + ); + + it.each` + startState | transitionEvent + ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR} + ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS} + ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR} + ${STATE_IDLE} | ${'RANDOM_FOO'} + ${STATE_LOADING} | ${TRANSITION_START_LOAD} + ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR} + ${STATE_LOADING} | ${'RANDOM_FOO'} + ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR} + ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS} + ${STATE_ERRORED} | ${'RANDOM_FOO'} + `( + 'remains as $startState if an undefined transition ($transitionEvent) is received', + ({ startState, transitionEvent }) => { + expect(transition(definition, startState, transitionEvent)).toEqual(startState); + }, + ); + + it('remains as the provided starting state if it is an unrecognized state', () => { + expect(transition(definition, 'RANDOM_FOO', TRANSITION_START_LOAD)).toEqual('RANDOM_FOO'); + }); + }); +}); diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index cad500039c0..beedb9b2eba 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -300,7 +300,7 @@ describe('init markdown', () => { }); }); - describe('Editor Lite', () => { + describe('Source Editor', () => { let editor; beforeEach(() => { diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 31c78681994..66d0faa95e7 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -24,6 +24,16 @@ const setWindowLocation = (value) => { }; describe('URL utility', () => { + let originalLocation; + + beforeAll(() => { + originalLocation = window.location; + }); + + afterAll(() => { + window.location = originalLocation; + }); + describe('webIDEUrl', () => { afterEach(() => { gon.relative_url_root = ''; @@ -319,19 +329,17 @@ describe('URL utility', () => { }); describe('doesHashExistInUrl', () => { - it('should return true when the given string exists in the URL hash', () => { + beforeEach(() => { setWindowLocation({ - href: 'https://gitlab.com/gitlab-org/gitlab-test/issues/1#note_1', + hash: 'https://gitlab.com/gitlab-org/gitlab-test/issues/1#note_1', }); + }); + it('should return true when the given string exists in the URL hash', () => { expect(urlUtils.doesHashExistInUrl('note_')).toBe(true); }); it('should return false when the given string does not exist in the URL hash', () => { - setWindowLocation({ - href: 'https://gitlab.com/gitlab-org/gitlab-test/issues/1#note_1', - }); - expect(urlUtils.doesHashExistInUrl('doesnotexist')).toBe(false); }); }); @@ -651,6 +659,45 @@ describe('URL utility', () => { }); }); + describe('urlParamsToArray', () => { + it('returns empty array for empty querystring', () => { + expect(urlUtils.urlParamsToArray('')).toEqual([]); + }); + + it('should decode params', () => { + expect(urlUtils.urlParamsToArray('?label_name%5B%5D=test')[0]).toBe('label_name[]=test'); + }); + + it('should remove the question mark from the search params', () => { + const paramsArray = urlUtils.urlParamsToArray('?test=thing'); + + expect(paramsArray[0][0]).not.toBe('?'); + }); + }); + + describe('urlParamsToObject', () => { + it('parses path for label with trailing +', () => { + // eslint-disable-next-line import/no-deprecated + expect(urlUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({ + label_name: ['label+'], + }); + }); + + it('parses path for milestone with trailing +', () => { + // eslint-disable-next-line import/no-deprecated + expect(urlUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({ + milestone_title: 'A+', + }); + }); + + it('parses path for search terms with spaces', () => { + // eslint-disable-next-line import/no-deprecated + expect(urlUtils.urlParamsToObject('search=two+words', {})).toEqual({ + search: 'two words', + }); + }); + }); + describe('queryToObject', () => { it.each` case | query | options | result @@ -673,12 +720,68 @@ describe('URL utility', () => { }); }); + describe('getParameterByName', () => { + const { getParameterByName } = urlUtils; + + it('should return valid parameter', () => { + setWindowLocation({ search: '?scope=all&p=2' }); + + expect(getParameterByName('p')).toEqual('2'); + expect(getParameterByName('scope')).toBe('all'); + }); + + it('should return invalid parameter', () => { + setWindowLocation({ search: '?scope=all&p=2' }); + + expect(getParameterByName('fakeParameter')).toBe(null); + }); + + it('should return a parameter with spaces', () => { + setWindowLocation({ search: '?search=my terms' }); + + expect(getParameterByName('search')).toBe('my terms'); + }); + + it('should return a parameter with encoded spaces', () => { + setWindowLocation({ search: '?search=my%20terms' }); + + expect(getParameterByName('search')).toBe('my terms'); + }); + + it('should return a parameter with plus signs as spaces', () => { + setWindowLocation({ search: '?search=my+terms' }); + + expect(getParameterByName('search')).toBe('my terms'); + }); + + it('should return valid parameters if search is provided', () => { + expect(getParameterByName('foo', 'foo=bar')).toBe('bar'); + expect(getParameterByName('foo', '?foo=bar')).toBe('bar'); + + expect(getParameterByName('manan', 'foo=bar&manan=canchu')).toBe('canchu'); + expect(getParameterByName('manan', '?foo=bar&manan=canchu')).toBe('canchu'); + }); + }); + describe('objectToQuery', () => { it('converts search query object back into a search query', () => { const searchQueryObject = { one: '1', two: '2' }; expect(urlUtils.objectToQuery(searchQueryObject)).toEqual('one=1&two=2'); }); + + it('returns empty string when `params` is undefined, null or empty string', () => { + expect(urlUtils.objectToQuery()).toBe(''); + expect(urlUtils.objectToQuery('')).toBe(''); + }); + + it('returns query string with values of `params`', () => { + const singleQueryParams = { foo: true }; + const multipleQueryParams = { foo: true, bar: true }; + + expect(urlUtils.objectToQuery(singleQueryParams)).toBe('foo=true'); + expect(urlUtils.objectToQuery(multipleQueryParams)).toBe('foo=true&bar=true'); + }); }); describe('cleanLeadingSeparator', () => { |