diff options
Diffstat (limited to 'spec/frontend/vue_shared/directives/tooltip_spec.js')
-rw-r--r-- | spec/frontend/vue_shared/directives/tooltip_spec.js | 169 |
1 files changed, 116 insertions, 53 deletions
diff --git a/spec/frontend/vue_shared/directives/tooltip_spec.js b/spec/frontend/vue_shared/directives/tooltip_spec.js index 9d3dd3c5f75..4217b8d3c02 100644 --- a/spec/frontend/vue_shared/directives/tooltip_spec.js +++ b/spec/frontend/vue_shared/directives/tooltip_spec.js @@ -1,42 +1,59 @@ import $ from 'jquery'; +import { escape } from 'lodash'; import { mount } from '@vue/test-utils'; import tooltip from '~/vue_shared/directives/tooltip'; +const DEFAULT_TOOLTIP_TEMPLATE = '<div v-tooltip :title="tooltip"></div>'; +const HTML_TOOLTIP_TEMPLATE = '<div v-tooltip data-html="true" :title="tooltip"></div>'; + describe('Tooltip directive', () => { - let vm; + let wrapper; + + function createTooltipContainer({ + template = DEFAULT_TOOLTIP_TEMPLATE, + text = 'some text', + } = {}) { + wrapper = mount( + { + directives: { tooltip }, + data: () => ({ tooltip: text }), + template, + }, + { attachToDocument: true }, + ); + } + + async function showTooltip() { + $(wrapper.vm.$el).tooltip('show'); + jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); + } + + function findTooltipInnerHtml() { + return document.querySelector('.tooltip-inner').innerHTML; + } + + function findTooltipHtml() { + return document.querySelector('.tooltip').innerHTML; + } afterEach(() => { - if (vm) { - vm.$destroy(); - } + wrapper.destroy(); + wrapper = null; }); describe('with a single tooltip', () => { - beforeEach(() => { - const wrapper = mount( - { - directives: { - tooltip, - }, - data() { - return { - tooltip: 'some text', - }; - }, - template: '<div v-tooltip :title="tooltip"></div>', - }, - { attachToDocument: true }, - ); - - vm = wrapper.vm; - }); - it('should have tooltip plugin applied', () => { - expect($(vm.$el).data('bs.tooltip')).toBeDefined(); + createTooltipContainer(); + + expect($(wrapper.vm.$el).data('bs.tooltip')).toBeDefined(); }); it('displays the title as tooltip', () => { - $(vm.$el).tooltip('show'); + createTooltipContainer(); + + $(wrapper.vm.$el).tooltip('show'); + jest.runOnlyPendingTimers(); const tooltipElement = document.querySelector('.tooltip-inner'); @@ -44,52 +61,98 @@ describe('Tooltip directive', () => { expect(tooltipElement.textContent).toContain('some text'); }); - it('updates a visible tooltip', () => { - $(vm.$el).tooltip('show'); + it.each` + condition | template | sanitize + ${'does not contain any html'} | ${DEFAULT_TOOLTIP_TEMPLATE} | ${false} + ${'contains html'} | ${HTML_TOOLTIP_TEMPLATE} | ${true} + `('passes sanitize=$sanitize if the tooltip $condition', ({ template, sanitize }) => { + createTooltipContainer({ template }); + + expect($(wrapper.vm.$el).data('bs.tooltip').config.sanitize).toEqual(sanitize); + }); + + it('updates a visible tooltip', async () => { + createTooltipContainer(); + + $(wrapper.vm.$el).tooltip('show'); jest.runOnlyPendingTimers(); const tooltipElement = document.querySelector('.tooltip-inner'); - vm.tooltip = 'other text'; + wrapper.vm.tooltip = 'other text'; jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); + + expect(tooltipElement.textContent).toContain('other text'); + }); + + describe('tooltip sanitization', () => { + it('reads tooltip content as text if data-html is not passed', async () => { + createTooltipContainer({ text: 'sample text<script>alert("XSS!!")</script>' }); - return vm.$nextTick().then(() => { - expect(tooltipElement.textContent).toContain('other text'); + await showTooltip(); + + const result = findTooltipInnerHtml(); + expect(result).toEqual('sample text<script>alert("XSS!!")</script>'); + }); + + it('sanitizes tooltip if data-html is passed', async () => { + createTooltipContainer({ + template: HTML_TOOLTIP_TEMPLATE, + text: 'sample text<script>alert("XSS!!")</script>', + }); + + await showTooltip(); + + const result = findTooltipInnerHtml(); + expect(result).toEqual('sample text'); + expect(result).not.toContain('XSS!!'); + }); + + it('sanitizes tooltip if data-template is passed', async () => { + const tooltipTemplate = escape( + '<div class="tooltip" role="tooltip"><div onclick="alert(\'XSS!\')" class="arrow"></div><div class="tooltip-inner"></div></div>', + ); + + createTooltipContainer({ + template: `<div v-tooltip :title="tooltip" data-html="false" data-template="${tooltipTemplate}"></div>`, + }); + + await showTooltip(); + + const result = findTooltipHtml(); + expect(result).toEqual( + // objectionable element is removed + '<div class="arrow"></div><div class="tooltip-inner">some text</div>', + ); + expect(result).not.toContain('XSS!!'); }); }); }); describe('with multiple tooltips', () => { beforeEach(() => { - const wrapper = mount( - { - directives: { - tooltip, - }, - template: ` - <div> - <div - v-tooltip - class="js-look-for-tooltip" - title="foo"> - </div> - <div - v-tooltip - title="bar"> - </div> + createTooltipContainer({ + template: ` + <div> + <div + v-tooltip + class="js-look-for-tooltip" + title="foo"> </div> - `, - }, - { attachToDocument: true }, - ); - - vm = wrapper.vm; + <div + v-tooltip + title="bar"> + </div> + </div> + `, + }); }); it('should have tooltip plugin applied to all instances', () => { expect( - $(vm.$el) + $(wrapper.vm.$el) .find('.js-look-for-tooltip') .data('bs.tooltip'), ).toBeDefined(); |