summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared/directives/tooltip_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/vue_shared/directives/tooltip_spec.js')
-rw-r--r--spec/frontend/vue_shared/directives/tooltip_spec.js169
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&lt;script&gt;alert("XSS!!")&lt;/script&gt;');
+ });
+
+ 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();