From 9f46488805e86b1bc341ea1620b866016c2ce5ed Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 20 May 2020 14:34:42 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-0-stable-ee --- spec/frontend/notebook/cells/code_spec.js | 90 +++++++++++ spec/frontend/notebook/cells/markdown_spec.js | 167 +++++++++++++++++++++ .../notebook/cells/output/html_sanitize_tests.js | 68 +++++++++ spec/frontend/notebook/cells/output/html_spec.js | 31 ++++ spec/frontend/notebook/cells/output/index_spec.js | 115 ++++++++++++++ spec/frontend/notebook/cells/prompt_spec.js | 56 +++++++ spec/frontend/notebook/index_spec.js | 100 ++++++++++++ 7 files changed, 627 insertions(+) create mode 100644 spec/frontend/notebook/cells/code_spec.js create mode 100644 spec/frontend/notebook/cells/markdown_spec.js create mode 100644 spec/frontend/notebook/cells/output/html_sanitize_tests.js create mode 100644 spec/frontend/notebook/cells/output/html_spec.js create mode 100644 spec/frontend/notebook/cells/output/index_spec.js create mode 100644 spec/frontend/notebook/cells/prompt_spec.js create mode 100644 spec/frontend/notebook/index_spec.js (limited to 'spec/frontend/notebook') diff --git a/spec/frontend/notebook/cells/code_spec.js b/spec/frontend/notebook/cells/code_spec.js new file mode 100644 index 00000000000..33dabe2b6dc --- /dev/null +++ b/spec/frontend/notebook/cells/code_spec.js @@ -0,0 +1,90 @@ +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 output', () => { + // NBFormat Version 4.1 allows outputs.text to be a string + beforeEach(() => { + const cell = json.cells[2]; + cell.outputs[0].text = cell.outputs[0].text.join(''); + + vm = setupComponent(cell); + return vm.$nextTick(); + }); + + 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(() => { + const cell = json.cells[0]; + cell.source = cell.source.join(''); + + vm = setupComponent(cell); + return vm.$nextTick(); + }); + + 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..ad33858da22 --- /dev/null +++ b/spec/frontend/notebook/cells/markdown_spec.js @@ -0,0 +1,167 @@ +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(() => { + json = getJSONFixture('blob/notebook/basic.json'); + + // eslint-disable-next-line prefer-destructuring + cell = json.cells[1]; + + vm = new Component({ + propsData: { + cell, + }, + }); + vm.$mount(); + + return vm.$nextTick(); + }); + + 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', () => { + Object.assign(cell, { + source: [ + '[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n', + ], + }); + + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull(); + }); + }); + + describe('katex', () => { + beforeEach(() => { + json = getJSONFixture('blob/notebook/math.json'); + }); + + it('renders multi-line katex', () => { + vm = new Component({ + propsData: { + cell: json.cells[0], + }, + }).$mount(); + + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('.katex')).not.toBeNull(); + }); + }); + + it('renders inline katex', () => { + vm = new Component({ + propsData: { + cell: json.cells[1], + }, + }).$mount(); + + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull(); + }); + }); + + it('renders multiple inline katex', () => { + vm = new Component({ + propsData: { + cell: json.cells[1], + }, + }).$mount(); + + return vm.$nextTick().then(() => { + expect(vm.$el.querySelectorAll('p:nth-child(2) .katex').length).toBe(4); + }); + }); + + it('output cell in case of katex error', () => { + vm = new Component({ + propsData: { + cell: { + cell_type: 'markdown', + metadata: {}, + source: ['Some invalid $a & b$ inline formula $b & c$\n', '\n'], + }, + }, + }).$mount(); + + return vm.$nextTick().then(() => { + // expect one paragraph with no katex formula in it + expect(vm.$el.querySelectorAll('p').length).toBe(1); + expect(vm.$el.querySelectorAll('p .katex').length).toBe(0); + }); + }); + + it('output cell and render remaining formula in case of katex error', () => { + vm = new Component({ + propsData: { + cell: { + cell_type: 'markdown', + metadata: {}, + source: ['An invalid $a & b$ inline formula and a vaild one $b = c$\n', '\n'], + }, + }, + }).$mount(); + + return vm.$nextTick().then(() => { + // expect one paragraph with no katex formula in it + expect(vm.$el.querySelectorAll('p').length).toBe(1); + expect(vm.$el.querySelectorAll('p .katex').length).toBe(1); + }); + }); + + it('renders math formula in list object', () => { + vm = new Component({ + propsData: { + cell: { + cell_type: 'markdown', + metadata: {}, + source: ["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n'], + }, + }, + }).$mount(); + + return vm.$nextTick().then(() => { + // expect one list with a katex formula in it + expect(vm.$el.querySelectorAll('li').length).toBe(1); + expect(vm.$el.querySelectorAll('li .katex').length).toBe(2); + }); + }); + + it("renders math formula with tick ' in it", () => { + vm = new Component({ + propsData: { + cell: { + cell_type: 'markdown', + metadata: {}, + source: ["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n'], + }, + }, + }).$mount(); + + return vm.$nextTick().then(() => { + // expect one list with a katex formula in it + expect(vm.$el.querySelectorAll('li').length).toBe(1); + expect(vm.$el.querySelectorAll('li .katex').length).toBe(2); + }); + }); + }); +}); 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: 'foo', + output: 'foo', + }, + 'protocol-based JS injection: simple, spaces before': { + input: 'foo', + output: 'foo', + }, + 'protocol-based JS injection: simple, spaces after': { + input: 'foo', + output: 'foo', + }, + 'protocol-based JS injection: simple, spaces before and after': { + input: 'foo', + output: 'foo', + }, + 'protocol-based JS injection: preceding colon': { + input: 'foo', + output: 'foo', + }, + 'protocol-based JS injection: UTF-8 encoding': { + input: 'foo', + output: 'foo', + }, + 'protocol-based JS injection: long UTF-8 encoding': { + input: 'foo', + output: 'foo', + }, + 'protocol-based JS injection: long UTF-8 encoding without semicolons': { + input: + 'foo', + output: 'foo', + }, + 'protocol-based JS injection: hex encoding': { + input: 'foo', + output: 'foo', + }, + 'protocol-based JS injection: long hex encoding': { + input: 'foo', + output: 'foo', + }, + 'protocol-based JS injection: hex encoding without semicolons': { + input: + 'foo', + output: 'foo', + }, + 'protocol-based JS injection: null char': { + input: 'foo', + output: 'foo', + }, + 'protocol-based JS injection: invalid URL char': { + input: '', + output: '', + }, + 'protocol-based JS injection: Unicode': { + input: 'foo', + output: 'foo', + }, + 'protocol-based JS injection: spaces and entities': { + input: 'foo', + output: 'foo', + }, + 'img on error': { + input: '', + output: '', + }, +}; 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(); + }); + }); +}); -- cgit v1.2.3