Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-20 17:34:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-20 17:34:42 +0300
commit9f46488805e86b1bc341ea1620b866016c2ce5ed (patch)
treef9748c7e287041e37d6da49e0a29c9511dc34768 /spec/frontend/notebook
parentdfc92d081ea0332d69c8aca2f0e745cb48ae5e6d (diff)
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
Diffstat (limited to 'spec/frontend/notebook')
-rw-r--r--spec/frontend/notebook/cells/code_spec.js90
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js167
-rw-r--r--spec/frontend/notebook/cells/output/html_sanitize_tests.js68
-rw-r--r--spec/frontend/notebook/cells/output/html_spec.js31
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js115
-rw-r--r--spec/frontend/notebook/cells/prompt_spec.js56
-rw-r--r--spec/frontend/notebook/index_spec.js100
7 files changed, 627 insertions, 0 deletions
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: '<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&#58;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ 'protocol-based JS injection: long UTF-8 encoding': {
+ input: '<a href="javascript&#0058;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ 'protocol-based JS injection: long UTF-8 encoding without semicolons': {
+ input:
+ '<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
+ output: '<a>foo</a>',
+ },
+ 'protocol-based JS injection: hex encoding': {
+ input: '<a href="javascript&#x3A;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ 'protocol-based JS injection: long hex encoding': {
+ input: '<a href="javascript&#x003A;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ 'protocol-based JS injection: hex encoding without semicolons': {
+ input:
+ '<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>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=" &#14; 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();
+ });
+ });
+});