diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-08 09:09:39 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-08 09:09:39 +0000 |
commit | 5bdbc604c8a08f827c3833e2c28ec0c299bb41fc (patch) | |
tree | 1fbf5e5dc3bff54d371baa15775371ebc8525f62 /spec/frontend | |
parent | 79ddf163588de2d9a7f1cc27262dc1a14503f619 (diff) | |
download | gitlab-ce-5bdbc604c8a08f827c3833e2c28ec0c299bb41fc.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
-rw-r--r-- | spec/frontend/design_management/router_spec.js | 79 | ||||
-rw-r--r-- | spec/frontend/design_management/utils/tracking_spec.js | 51 | ||||
-rw-r--r-- | spec/frontend/diffs/components/diff_content_spec.js | 2 | ||||
-rw-r--r-- | spec/frontend/notebook/cells/code_spec.js | 74 | ||||
-rw-r--r-- | spec/frontend/notebook/cells/markdown_spec.js | 105 | ||||
-rw-r--r-- | spec/frontend/notebook/cells/output/html_sanitize_tests.js | 68 | ||||
-rw-r--r-- | spec/frontend/notebook/cells/output/html_spec.js | 31 | ||||
-rw-r--r-- | spec/frontend/notebook/cells/output/index_spec.js | 115 | ||||
-rw-r--r-- | spec/frontend/notebook/cells/prompt_spec.js | 56 | ||||
-rw-r--r-- | spec/frontend/notebook/index_spec.js | 100 |
10 files changed, 637 insertions, 44 deletions
diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js index fc88bfa06d2..0f4afa5e288 100644 --- a/spec/frontend/design_management/router_spec.js +++ b/spec/frontend/design_management/router_spec.js @@ -1,4 +1,5 @@ import { mount, createLocalVue } from '@vue/test-utils'; +import { nextTick } from 'vue'; import VueRouter from 'vue-router'; import App from '~/design_management/components/app.vue'; import Designs from '~/design_management/pages/index.vue'; @@ -11,74 +12,66 @@ import { } from '~/design_management/router/constants'; import '~/commons/bootstrap'; -jest.mock('mousetrap', () => ({ - bind: jest.fn(), - unbind: jest.fn(), -})); - -describe('Design management router', () => { - let vm; - let router; - - function factory() { - const localVue = createLocalVue(); - - localVue.use(VueRouter); +function factory(routeArg) { + const localVue = createLocalVue(); + localVue.use(VueRouter); - window.gon = { sprite_icons: '' }; + window.gon = { sprite_icons: '' }; - router = createRouter('/'); + const router = createRouter('/'); + if (routeArg !== undefined) { + router.push(routeArg); + } - vm = mount(App, { - localVue, - router, - mocks: { - $apollo: { - queries: { - designs: { loading: true }, - design: { loading: true }, - permissions: { loading: true }, - }, + return mount(App, { + localVue, + router, + mocks: { + $apollo: { + queries: { + designs: { loading: true }, + design: { loading: true }, + permissions: { loading: true }, }, }, - }); - } - - beforeEach(() => { - factory(); + }, }); +} - afterEach(() => { - vm.destroy(); +jest.mock('mousetrap', () => ({ + bind: jest.fn(), + unbind: jest.fn(), +})); - router.app.$destroy(); +describe('Design management router', () => { + afterEach(() => { window.location.hash = ''; }); - describe.each([['/'], [{ name: ROOT_ROUTE_NAME }]])('root route', pushArg => { + describe.each([['/'], [{ name: ROOT_ROUTE_NAME }]])('root route', routeArg => { it('pushes home component', () => { - router.push(pushArg); + const wrapper = factory(routeArg); - expect(vm.find(Designs).exists()).toBe(true); + expect(wrapper.find(Designs).exists()).toBe(true); }); }); - describe.each([['/designs'], [{ name: DESIGNS_ROUTE_NAME }]])('designs route', pushArg => { + describe.each([['/designs'], [{ name: DESIGNS_ROUTE_NAME }]])('designs route', routeArg => { it('pushes designs root component', () => { - router.push(pushArg); + const wrapper = factory(routeArg); - expect(vm.find(Designs).exists()).toBe(true); + expect(wrapper.find(Designs).exists()).toBe(true); }); }); describe.each([['/designs/1'], [{ name: DESIGN_ROUTE_NAME, params: { id: '1' } }]])( 'designs detail route', - pushArg => { + routeArg => { it('pushes designs detail component', () => { - router.push(pushArg); + const wrapper = factory(routeArg); - return vm.vm.$nextTick().then(() => { - const detail = vm.find(DesignDetail); + return nextTick().then(() => { + const detail = wrapper.find(DesignDetail); expect(detail.exists()).toBe(true); expect(detail.props('id')).toEqual('1'); }); diff --git a/spec/frontend/design_management/utils/tracking_spec.js b/spec/frontend/design_management/utils/tracking_spec.js new file mode 100644 index 00000000000..ab540587c01 --- /dev/null +++ b/spec/frontend/design_management/utils/tracking_spec.js @@ -0,0 +1,51 @@ +import { mockTracking } from 'helpers/tracking_helper'; +import { trackDesignDetailView } from '~/design_management/utils/tracking'; + +function getTrackingSpy(key) { + return mockTracking(key, undefined, jest.spyOn); +} + +describe('Tracking Events', () => { + describe('trackDesignDetailView', () => { + const eventKey = 'projects:issues:design'; + const eventName = 'design_viewed'; + + it('trackDesignDetailView fires a tracking event when called', () => { + const trackingSpy = getTrackingSpy(eventKey); + + trackDesignDetailView(); + + expect(trackingSpy).toHaveBeenCalledWith( + eventKey, + eventName, + expect.objectContaining({ + label: eventName, + value: { + 'internal-object-refrerer': '', + 'version-number': 1, + 'current-version': false, + }, + }), + ); + }); + + it('trackDesignDetailView allows to customize the value payload', () => { + const trackingSpy = getTrackingSpy(eventKey); + + trackDesignDetailView('from-a-test', 100, true); + + expect(trackingSpy).toHaveBeenCalledWith( + eventKey, + eventName, + expect.objectContaining({ + label: eventName, + value: { + 'internal-object-refrerer': 'from-a-test', + 'version-number': 100, + 'current-version': true, + }, + }), + ); + }); + }); +}); diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js index 979c67787f7..b78895f9e55 100644 --- a/spec/frontend/diffs/components/diff_content_spec.js +++ b/spec/frontend/diffs/components/diff_content_spec.js @@ -10,7 +10,7 @@ import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue'; import NoteForm from '~/notes/components/note_form.vue'; import DiffDiscussions from '~/diffs/components/diff_discussions.vue'; import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants'; -import diffFileMockData from '../../../javascripts/diffs/mock_data/diff_file'; +import diffFileMockData from '../mock_data/diff_file'; import { diffViewerModes } from '~/ide/constants'; const localVue = createLocalVue(); diff --git a/spec/frontend/notebook/cells/code_spec.js b/spec/frontend/notebook/cells/code_spec.js new file mode 100644 index 00000000000..3c9aea4a61a --- /dev/null +++ b/spec/frontend/notebook/cells/code_spec.js @@ -0,0 +1,74 @@ +import Vue from 'vue'; +import CodeComponent from '~/notebook/cells/code.vue'; + +const Component = Vue.extend(CodeComponent); + +describe('Code component', () => { + let vm; + let json; + + beforeEach(() => { + json = getJSONFixture('blob/notebook/basic.json'); + }); + + const setupComponent = cell => { + const comp = new Component({ + propsData: { + cell, + }, + }); + comp.$mount(); + return comp; + }; + + describe('without output', () => { + beforeEach(done => { + vm = setupComponent(json.cells[0]); + + setImmediate(() => { + done(); + }); + }); + + it('does not render output prompt', () => { + expect(vm.$el.querySelectorAll('.prompt').length).toBe(1); + }); + }); + + describe('with output', () => { + beforeEach(done => { + vm = setupComponent(json.cells[2]); + + setImmediate(() => { + done(); + }); + }); + + it('does not render output prompt', () => { + expect(vm.$el.querySelectorAll('.prompt').length).toBe(2); + }); + + it('renders output cell', () => { + expect(vm.$el.querySelector('.output')).toBeDefined(); + }); + }); + + describe('with string for cell.source', () => { + beforeEach(done => { + const cell = json.cells[0]; + cell.source = cell.source.join(''); + + vm = setupComponent(cell); + + setImmediate(() => { + done(); + }); + }); + + it('renders the same input as when cell.source is an array', () => { + const expected = "console.log('test')"; + + expect(vm.$el.querySelector('.input').innerText).toContain(expected); + }); + }); +}); diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js new file mode 100644 index 00000000000..359ac90a3ef --- /dev/null +++ b/spec/frontend/notebook/cells/markdown_spec.js @@ -0,0 +1,105 @@ +import Vue from 'vue'; +import katex from 'katex'; +import MarkdownComponent from '~/notebook/cells/markdown.vue'; + +const Component = Vue.extend(MarkdownComponent); + +window.katex = katex; + +describe('Markdown component', () => { + let vm; + let cell; + let json; + + beforeEach(done => { + json = getJSONFixture('blob/notebook/basic.json'); + + // eslint-disable-next-line prefer-destructuring + cell = json.cells[1]; + + vm = new Component({ + propsData: { + cell, + }, + }); + vm.$mount(); + + setImmediate(() => { + done(); + }); + }); + + it('does not render promot', () => { + expect(vm.$el.querySelector('.prompt span')).toBeNull(); + }); + + it('does not render the markdown text', () => { + expect(vm.$el.querySelector('.markdown').innerHTML.trim()).not.toEqual(cell.source.join('')); + }); + + it('renders the markdown HTML', () => { + expect(vm.$el.querySelector('.markdown h1')).not.toBeNull(); + }); + + it('sanitizes output', done => { + Object.assign(cell, { + source: [ + '[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n', + ], + }); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull(); + + done(); + }); + }); + + describe('katex', () => { + beforeEach(() => { + json = getJSONFixture('blob/notebook/math.json'); + }); + + it('renders multi-line katex', done => { + vm = new Component({ + propsData: { + cell: json.cells[0], + }, + }).$mount(); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.katex')).not.toBeNull(); + + done(); + }); + }); + + it('renders inline katex', done => { + vm = new Component({ + propsData: { + cell: json.cells[1], + }, + }).$mount(); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull(); + + done(); + }); + }); + + it('renders multiple inline katex', done => { + vm = new Component({ + propsData: { + cell: json.cells[1], + }, + }).$mount(); + + Vue.nextTick(() => { + expect(vm.$el.querySelectorAll('p:nth-child(2) .katex').length).toBe(4); + + done(); + }); + }); + }); +}); diff --git a/spec/frontend/notebook/cells/output/html_sanitize_tests.js b/spec/frontend/notebook/cells/output/html_sanitize_tests.js new file mode 100644 index 00000000000..74c48f04367 --- /dev/null +++ b/spec/frontend/notebook/cells/output/html_sanitize_tests.js @@ -0,0 +1,68 @@ +export default { + 'protocol-based JS injection: simple, no spaces': { + input: '<a href="javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces before': { + input: '<a href="javascript :alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces after': { + input: '<a href="javascript: alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces before and after': { + input: '<a href="javascript : alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: preceding colon': { + input: '<a href=":javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: UTF-8 encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long UTF-8 encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long UTF-8 encoding without semicolons': { + input: + '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: hex encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long hex encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: hex encoding without semicolons': { + input: + '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: null char': { + input: '<a href=java\0script:alert("XSS")>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: invalid URL char': { + input: '<img src=javascript:alert("XSS")>', + output: '<img>', + }, + 'protocol-based JS injection: Unicode': { + input: '<a href="\u0001java\u0003script:alert(\'XSS\')">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: spaces and entities': { + input: '<a href="  javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'img on error': { + input: '<img src="x" onerror="alert(document.domain)" />', + output: '<img src="x">', + }, +}; diff --git a/spec/frontend/notebook/cells/output/html_spec.js b/spec/frontend/notebook/cells/output/html_spec.js new file mode 100644 index 00000000000..3ee404fb187 --- /dev/null +++ b/spec/frontend/notebook/cells/output/html_spec.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import htmlOutput from '~/notebook/cells/output/html.vue'; +import sanitizeTests from './html_sanitize_tests'; + +describe('html output cell', () => { + function createComponent(rawCode) { + const Component = Vue.extend(htmlOutput); + + return new Component({ + propsData: { + rawCode, + count: 0, + index: 0, + }, + }).$mount(); + } + + describe('sanitizes output', () => { + Object.keys(sanitizeTests).forEach(key => { + it(key, () => { + const test = sanitizeTests[key]; + const vm = createComponent(test.input); + const outputEl = [...vm.$el.querySelectorAll('div')].pop(); + + expect(outputEl.innerHTML).toEqual(test.output); + + vm.$destroy(); + }); + }); + }); +}); diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js new file mode 100644 index 00000000000..2b1aa5317c5 --- /dev/null +++ b/spec/frontend/notebook/cells/output/index_spec.js @@ -0,0 +1,115 @@ +import Vue from 'vue'; +import CodeComponent from '~/notebook/cells/output/index.vue'; + +const Component = Vue.extend(CodeComponent); + +describe('Output component', () => { + let vm; + let json; + + const createComponent = output => { + vm = new Component({ + propsData: { + outputs: [].concat(output), + count: 1, + }, + }); + vm.$mount(); + }; + + beforeEach(() => { + json = getJSONFixture('blob/notebook/basic.json'); + }); + + describe('text output', () => { + beforeEach(done => { + createComponent(json.cells[2].outputs[0]); + + setImmediate(() => { + done(); + }); + }); + + it('renders as plain text', () => { + expect(vm.$el.querySelector('pre')).not.toBeNull(); + }); + + it('renders promot', () => { + expect(vm.$el.querySelector('.prompt span')).not.toBeNull(); + }); + }); + + describe('image output', () => { + beforeEach(done => { + createComponent(json.cells[3].outputs[0]); + + setImmediate(() => { + done(); + }); + }); + + it('renders as an image', () => { + expect(vm.$el.querySelector('img')).not.toBeNull(); + }); + }); + + describe('html output', () => { + it('renders raw HTML', () => { + createComponent(json.cells[4].outputs[0]); + + expect(vm.$el.querySelector('p')).not.toBeNull(); + expect(vm.$el.querySelectorAll('p').length).toBe(1); + expect(vm.$el.textContent.trim()).toContain('test'); + }); + + it('renders multiple raw HTML outputs', () => { + createComponent([json.cells[4].outputs[0], json.cells[4].outputs[0]]); + + expect(vm.$el.querySelectorAll('p').length).toBe(2); + }); + }); + + describe('svg output', () => { + beforeEach(done => { + createComponent(json.cells[5].outputs[0]); + + setImmediate(() => { + done(); + }); + }); + + it('renders as an svg', () => { + expect(vm.$el.querySelector('svg')).not.toBeNull(); + }); + }); + + describe('default to plain text', () => { + beforeEach(done => { + createComponent(json.cells[6].outputs[0]); + + setImmediate(() => { + done(); + }); + }); + + it('renders as plain text', () => { + expect(vm.$el.querySelector('pre')).not.toBeNull(); + expect(vm.$el.textContent.trim()).toContain('testing'); + }); + + it('renders promot', () => { + expect(vm.$el.querySelector('.prompt span')).not.toBeNull(); + }); + + it("renders as plain text when doesn't recognise other types", done => { + createComponent(json.cells[7].outputs[0]); + + setImmediate(() => { + expect(vm.$el.querySelector('pre')).not.toBeNull(); + expect(vm.$el.textContent.trim()).toContain('testing'); + + done(); + }); + }); + }); +}); diff --git a/spec/frontend/notebook/cells/prompt_spec.js b/spec/frontend/notebook/cells/prompt_spec.js new file mode 100644 index 00000000000..cf5a7a603c6 --- /dev/null +++ b/spec/frontend/notebook/cells/prompt_spec.js @@ -0,0 +1,56 @@ +import Vue from 'vue'; +import PromptComponent from '~/notebook/cells/prompt.vue'; + +const Component = Vue.extend(PromptComponent); + +describe('Prompt component', () => { + let vm; + + describe('input', () => { + beforeEach(done => { + vm = new Component({ + propsData: { + type: 'In', + count: 1, + }, + }); + vm.$mount(); + + setImmediate(() => { + done(); + }); + }); + + it('renders in label', () => { + expect(vm.$el.textContent.trim()).toContain('In'); + }); + + it('renders count', () => { + expect(vm.$el.textContent.trim()).toContain('1'); + }); + }); + + describe('output', () => { + beforeEach(done => { + vm = new Component({ + propsData: { + type: 'Out', + count: 1, + }, + }); + vm.$mount(); + + setImmediate(() => { + done(); + }); + }); + + it('renders in label', () => { + expect(vm.$el.textContent.trim()).toContain('Out'); + }); + + it('renders count', () => { + expect(vm.$el.textContent.trim()).toContain('1'); + }); + }); +}); diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js new file mode 100644 index 00000000000..36b092be976 --- /dev/null +++ b/spec/frontend/notebook/index_spec.js @@ -0,0 +1,100 @@ +import Vue from 'vue'; +import Notebook from '~/notebook/index.vue'; + +const Component = Vue.extend(Notebook); + +describe('Notebook component', () => { + let vm; + let json; + let jsonWithWorksheet; + + beforeEach(() => { + json = getJSONFixture('blob/notebook/basic.json'); + jsonWithWorksheet = getJSONFixture('blob/notebook/worksheets.json'); + }); + + describe('without JSON', () => { + beforeEach(done => { + vm = new Component({ + propsData: { + notebook: {}, + }, + }); + vm.$mount(); + + setImmediate(() => { + done(); + }); + }); + + it('does not render', () => { + expect(vm.$el.tagName).toBeUndefined(); + }); + }); + + describe('with JSON', () => { + beforeEach(done => { + vm = new Component({ + propsData: { + notebook: json, + codeCssClass: 'js-code-class', + }, + }); + vm.$mount(); + + setImmediate(() => { + done(); + }); + }); + + it('renders cells', () => { + expect(vm.$el.querySelectorAll('.cell').length).toBe(json.cells.length); + }); + + it('renders markdown cell', () => { + expect(vm.$el.querySelector('.markdown')).not.toBeNull(); + }); + + it('renders code cell', () => { + expect(vm.$el.querySelector('pre')).not.toBeNull(); + }); + + it('add code class to code blocks', () => { + expect(vm.$el.querySelector('.js-code-class')).not.toBeNull(); + }); + }); + + describe('with worksheets', () => { + beforeEach(done => { + vm = new Component({ + propsData: { + notebook: jsonWithWorksheet, + codeCssClass: 'js-code-class', + }, + }); + vm.$mount(); + + setImmediate(() => { + done(); + }); + }); + + it('renders cells', () => { + expect(vm.$el.querySelectorAll('.cell').length).toBe( + jsonWithWorksheet.worksheets[0].cells.length, + ); + }); + + it('renders markdown cell', () => { + expect(vm.$el.querySelector('.markdown')).not.toBeNull(); + }); + + it('renders code cell', () => { + expect(vm.$el.querySelector('pre')).not.toBeNull(); + }); + + it('add code class to code blocks', () => { + expect(vm.$el.querySelector('.js-code-class')).not.toBeNull(); + }); + }); +}); |