diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-20 11:43:17 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-20 11:43:17 +0000 |
commit | dfc94207fec2d84314b1a5410cface22e8b369bd (patch) | |
tree | c54022f61ced104305889a64de080998a0dc773b /spec/frontend/notebook | |
parent | b874efeff674f6bf0355d5d242ecf81c6f7155df (diff) | |
download | gitlab-ce-dfc94207fec2d84314b1a5410cface22e8b369bd.tar.gz |
Add latest changes from gitlab-org/gitlab@15-11-stable-eev15.11.0-rc42
Diffstat (limited to 'spec/frontend/notebook')
-rw-r--r-- | spec/frontend/notebook/cells/output/dataframe_spec.js | 59 | ||||
-rw-r--r-- | spec/frontend/notebook/cells/output/dataframe_util_spec.js | 113 | ||||
-rw-r--r-- | spec/frontend/notebook/cells/output/index_spec.js | 18 | ||||
-rw-r--r-- | spec/frontend/notebook/mock_data.js | 44 |
4 files changed, 233 insertions, 1 deletions
diff --git a/spec/frontend/notebook/cells/output/dataframe_spec.js b/spec/frontend/notebook/cells/output/dataframe_spec.js new file mode 100644 index 00000000000..abf6631353c --- /dev/null +++ b/spec/frontend/notebook/cells/output/dataframe_spec.js @@ -0,0 +1,59 @@ +import { shallowMount } from '@vue/test-utils'; +import DataframeOutput from '~/notebook/cells/output/dataframe.vue'; +import JSONTable from '~/behaviors/components/json_table.vue'; +import { outputWithDataframe } from '../../mock_data'; + +describe('~/notebook/cells/output/DataframeOutput', () => { + let wrapper; + + function createComponent(rawCode) { + wrapper = shallowMount(DataframeOutput, { + propsData: { + rawCode, + count: 0, + index: 0, + }, + }); + } + + const findTable = () => wrapper.findComponent(JSONTable); + + describe('with valid dataframe', () => { + beforeEach(() => createComponent(outputWithDataframe.data['text/html'].join(''))); + + it('mounts the table', () => { + expect(findTable().exists()).toBe(true); + }); + + it('table caption is empty', () => { + expect(findTable().props().caption).toEqual(''); + }); + + it('allows filtering', () => { + expect(findTable().props().hasFilter).toBe(true); + }); + + it('sets the correct fields', () => { + expect(findTable().props().fields).toEqual([ + { key: 'index', label: '', sortable: true }, + { key: 'column_1', label: 'column_1', sortable: true }, + { key: 'column_2', label: 'column_2', sortable: true }, + ]); + }); + + it('sets the correct items', () => { + expect(findTable().props().items).toEqual([ + { index: 0, column_1: 'abc de f', column_2: 'a' }, + { index: 1, column_1: 'True', column_2: '0.1' }, + ]); + }); + }); + + describe('invalid dataframe', () => { + it('still displays the table', () => { + createComponent('dataframe'); + + expect(findTable().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/notebook/cells/output/dataframe_util_spec.js b/spec/frontend/notebook/cells/output/dataframe_util_spec.js new file mode 100644 index 00000000000..ddc1b3cfe26 --- /dev/null +++ b/spec/frontend/notebook/cells/output/dataframe_util_spec.js @@ -0,0 +1,113 @@ +import { isDataframe, convertHtmlTableToJson } from '~/notebook/cells/output/dataframe_util'; +import { outputWithDataframeContent } from '../../mock_data'; +import sanitizeTests from './html_sanitize_fixtures'; + +describe('notebook/cells/output/dataframe_utils', () => { + describe('isDataframe', () => { + describe('when output data has no text/html', () => { + it('is is not a dataframe', () => { + const input = { data: { 'image/png': ['blah'] } }; + + expect(isDataframe(input)).toBe(false); + }); + }); + + describe('when output data has no text/html, but no mention of dataframe', () => { + it('is is not a dataframe', () => { + const input = { data: { 'text/html': ['blah'] } }; + + expect(isDataframe(input)).toBe(false); + }); + }); + + describe('when output data has text/html, but no mention of dataframe in the first 20 lines', () => { + it('is is not a dataframe', () => { + const input = { data: { 'text/html': [...new Array(20).fill('a'), 'dataframe'] } }; + + expect(isDataframe(input)).toBe(false); + }); + }); + + describe('when output data has text/html, and includes "dataframe" within the first 20 lines', () => { + it('is is not a dataframe', () => { + const input = { data: { 'text/html': ['dataframe'] } }; + + expect(isDataframe(input)).toBe(true); + }); + }); + }); + + describe('convertHtmlTableToJson', () => { + it('converts table correctly', () => { + const input = outputWithDataframeContent; + + const output = { + fields: [ + { key: 'index', label: '', sortable: true }, + { key: 'column_1', label: 'column_1', sortable: true }, + { key: 'column_2', label: 'column_2', sortable: true }, + ], + items: [ + { index: 0, column_1: 'abc de f', column_2: 'a' }, + { index: 1, column_1: 'True', column_2: '0.1' }, + ], + }; + + expect(convertHtmlTableToJson(input)).toEqual(output); + }); + + describe('sanitizes input before parsing table', () => { + it('sanitizes input html', () => { + const parser = new DOMParser(); + const spy = jest.spyOn(parser, 'parseFromString'); + const input = 'hello<style>p {width:50%;}</style><script>alert(1)</script>'; + + convertHtmlTableToJson(input, parser); + + expect(spy).toHaveBeenCalledWith('hello', 'text/html'); + }); + }); + + describe('does not include harmful html', () => { + const makeDataframeWithHtml = (html) => { + return [ + '<table border="1" class="dataframe">\n', + ' <thead>\n', + ' <tr style="text-align: right;">\n', + ' <th></th>\n', + ' <th>column_1</th>\n', + ' </tr>\n', + ' </thead>\n', + ' <tbody>\n', + ' <tr>\n', + ' <th>0</th>\n', + ` <td>${html}</td>\n`, + ' </tr>\n', + ' </tbody>\n', + '</table>\n', + '</div>', + ]; + }; + + it.each([ + ['table', 0], + ['style', 1], + ['iframe', 2], + ['svg', 3], + ])('sanitizes output for: %p', (tag, index) => { + const inputHtml = makeDataframeWithHtml(sanitizeTests[index][1].input); + const convertedHtml = convertHtmlTableToJson(inputHtml).items[0].column_1; + + expect(convertedHtml).not.toContain(tag); + }); + }); + + describe('when dataframe is invalid', () => { + it('returns empty', () => { + const input = [' dataframe', ' blah']; + + expect(convertHtmlTableToJson(input)).toEqual({ fields: [], items: [] }); + }); + }); + }); +}); diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js index 1241c133b89..efbdfca8d8c 100644 --- a/spec/frontend/notebook/cells/output/index_spec.js +++ b/spec/frontend/notebook/cells/output/index_spec.js @@ -2,7 +2,13 @@ import { mount } from '@vue/test-utils'; import json from 'test_fixtures/blob/notebook/basic.json'; import Output from '~/notebook/cells/output/index.vue'; import MarkdownOutput from '~/notebook/cells/output/markdown.vue'; -import { relativeRawPath, markdownCellContent } from '../../mock_data'; +import DataframeOutput from '~/notebook/cells/output/dataframe.vue'; +import { + relativeRawPath, + markdownCellContent, + outputWithDataframe, + outputWithDataframeContent, +} from '../../mock_data'; describe('Output component', () => { let wrapper; @@ -105,6 +111,16 @@ describe('Output component', () => { }); }); + describe('Dataframe output', () => { + it('renders DataframeOutput component', () => { + createComponent(outputWithDataframe); + + expect(wrapper.findComponent(DataframeOutput).props('rawCode')).toBe( + outputWithDataframeContent.join(''), + ); + }); + }); + describe('default to plain text', () => { beforeEach(() => { const unknownType = json.cells[6]; diff --git a/spec/frontend/notebook/mock_data.js b/spec/frontend/notebook/mock_data.js index 5c47cb5aa9b..15db2931b3c 100644 --- a/spec/frontend/notebook/mock_data.js +++ b/spec/frontend/notebook/mock_data.js @@ -6,3 +6,47 @@ export const errorOutputContent = [ '\u001b[0;32m/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_79203/294318627.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mTo\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m', "\u001b[0;31mNameError\u001b[0m: name 'To' is not defined", ]; +export const outputWithDataframeContent = [ + '<div>\n', + '<style scoped>\n', + ' .dataframe tbody tr th:only-of-type {\n', + ' vertical-align: middle;\n', + ' }\n', + '\n', + ' .dataframe tbody tr th {\n', + ' vertical-align: top;\n', + ' }\n', + '\n', + ' .dataframe thead th {\n', + ' text-align: right;\n', + ' }\n', + '</style>\n', + '<table border="1" class="dataframe">\n', + ' <thead>\n', + ' <tr style="text-align: right;">\n', + ' <th></th>\n', + ' <th>column_1</th>\n', + ' <th>column_2</th>\n', + ' </tr>\n', + ' </thead>\n', + ' <tbody>\n', + ' <tr>\n', + ' <th>0</th>\n', + ' <td>abc de f</td>\n', + ' <td>a</td>\n', + ' </tr>\n', + ' <tr>\n', + ' <th>1</th>\n', + ' <td>True</td>\n', + ' <td>0.1</td>\n', + ' </tr>\n', + ' </tbody>\n', + '</table>\n', + '</div>', +]; + +export const outputWithDataframe = { + data: { + 'text/html': outputWithDataframeContent, + }, +}; |