From 6226757ade6637f37f3bac321805142f6ebac523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 17 Jan 2019 15:08:06 -0500 Subject: [PATCH 1/6] generalize svgTextUtils.plainText - so that it can selective allow some pseudo-html tags to go through - add truncate logic where allowed pseudo-html tags don't count toward the output "length" --- src/lib/svg_text_utils.js | 66 ++++++++++++++++++++--- test/jasmine/tests/svg_text_utils_test.js | 37 +++++++++++++ 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index 6f269da7c32..c2ddb368251 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -264,8 +264,6 @@ var ZERO_WIDTH_SPACE = '\u200b'; */ var PROTOCOLS = ['http:', 'https:', 'mailto:', '', undefined, ':']; -var STRIP_TAGS = new RegExp(']*)?/?>', 'g'); - var NEWLINES = /(\r\n?|\n)/g; var SPLIT_TAGS = /(<[^<>]*>)/; @@ -315,10 +313,66 @@ function getQuotedMatch(_str, re) { var COLORMATCH = /(^|;)\s*color:/; -exports.plainText = function(_str) { - // strip out our pseudo-html so we have a readable - // version to put into text fields - return (_str || '').replace(STRIP_TAGS, ' '); +/** + * Strip string of tags + * + * @param {string} _str : input string + * @param {object} opts : + * - maxLen {number} max length of output string + * - allowedTags {array} list of pseudo-html tags to NOT strip + * @return {string} + */ +exports.plainText = function(_str, opts) { + opts = opts || {}; + + var len = (opts.len !== undefined && opts.len !== -1) ? opts.len : Infinity; + var allowedTags = opts.allowedTags !== undefined ? opts.allowedTags : ['br']; + + var ellipsis = '...'; + var eLen = ellipsis.length; + + var oldParts = _str.split(SPLIT_TAGS); + var newParts = []; + var prevTag = ''; + var l = 0; + + for(var i = 0; i < oldParts.length; i++) { + var p = oldParts[i]; + var match = p.match(ONE_TAG); + var tagType = match && match[2].toLowerCase(); + + if(tagType) { + // N.B. tags do not count towards string length + if(allowedTags.indexOf(tagType) !== -1) { + newParts.push(p); + prevTag = tagType; + } + } else { + var pLen = p.length; + + if((l + pLen) < len) { + newParts.push(p); + l += pLen; + } else if(l < len) { + var pLen2 = len - l; + + if(prevTag && (prevTag !== 'br' || pLen2 <= eLen || pLen <= eLen)) { + newParts.pop(); + } + + if(len > eLen) { + newParts.push(p.substr(0, pLen2 - eLen) + ellipsis); + } else { + newParts.push(p.substr(0, pLen2)); + } + break; + } + + prevTag = ''; + } + } + + return newParts.join(''); }; /* diff --git a/test/jasmine/tests/svg_text_utils_test.js b/test/jasmine/tests/svg_text_utils_test.js index 3c49cd033d3..6610f189ccd 100644 --- a/test/jasmine/tests/svg_text_utils_test.js +++ b/test/jasmine/tests/svg_text_utils_test.js @@ -468,4 +468,41 @@ describe('svg+text utils', function() { expect(node.text()).toEqual('test\u200b5\u200bmore'); }); }); + + describe('plainText:', function() { + var fn = util.plainText; + + it('should strip tags except
by default', function() { + expect(fn('ab
tma')).toBe('ab
tma'); + }); + + it('should work in various cases w/o
', function() { + var sIn = 'ThisIsDATA300'; + + expect(fn(sIn)).toBe('ThisIsDATA300'); + expect(fn(sIn, {len: 3})).toBe('Thi'); + expect(fn(sIn, {len: 4})).toBe('T...'); + expect(fn(sIn, {len: 13})).toBe('ThisIsDATA...'); + expect(fn(sIn, {len: 16})).toBe('ThisIsDATA300'); + expect(fn(sIn, {allowedTags: ['sup']})).toBe('ThisIsDATA300'); + expect(fn(sIn, {len: 13, allowedTags: ['sup']})).toBe('ThisIsDATA...'); + expect(fn(sIn, {len: 16, allowedTags: ['sup']})).toBe('ThisIsDATA300'); + }); + + it('should work in various cases w/
', function() { + var sIn = 'ThisIs
DATA300'; + + expect(fn(sIn)).toBe('ThisIs
DATA300'); + expect(fn(sIn, {len: 3})).toBe('Thi'); + expect(fn(sIn, {len: 4})).toBe('T...'); + expect(fn(sIn, {len: 7})).toBe('ThisIs...'); + expect(fn(sIn, {len: 8})).toBe('ThisIs...'); + expect(fn(sIn, {len: 9})).toBe('ThisIs...'); + expect(fn(sIn, {len: 10})).toBe('ThisIs
D...'); + expect(fn(sIn, {len: 13})).toBe('ThisIs
DATA...'); + expect(fn(sIn, {len: 16})).toBe('ThisIs
DATA300'); + expect(fn(sIn, {allowedTags: ['sup']})).toBe('ThisIsDATA300'); + expect(fn(sIn, {allowedTags: ['br', 'sup']})).toBe('ThisIs
DATA300'); + }); + }); }); From 72a99151335bf19986eb1ebcf529cc06f30003e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 17 Jan 2019 15:08:50 -0500 Subject: [PATCH 2/6] use new svgTextUtils.plainText opts for (aka name) hover label --- src/components/fx/hover.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 40020c4ef0f..d49265dee30 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -925,15 +925,10 @@ function createHoverText(hoverData, opts, gd) { if(d.nameOverride !== undefined) d.name = d.nameOverride; if(d.name) { - // strip out our pseudo-html elements from d.name (if it exists at all) - name = svgTextUtils.plainText(d.name || ''); - - var nameLength = Math.round(d.nameLength); - - if(nameLength > -1 && name.length > nameLength) { - if(nameLength > 3) name = name.substr(0, nameLength - 3) + '...'; - else name = name.substr(0, nameLength); - } + name = svgTextUtils.plainText(d.name || '', { + len: d.nameLength, + allowedTags: ['br', 'sub', 'sup'] + }); } if(d.zLabel !== undefined) { From 2837ca7ba5371e1fc65a23c039b763878a61fbd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 17 Jan 2019 15:10:11 -0500 Subject: [PATCH 3/6] tweak XSS via hover label test - the malicious tag is now gone! --- test/jasmine/tests/hover_label_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 3bae88b6024..2e85fa0016e 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -248,7 +248,7 @@ describe('hover info', function() { assertHoverLabelContent({ nums: '1\nhover text', - name: '<img src=x o...', + name: '', axis: '0.388' }); }); From beaaf1601784917493ebf89f8188563bc9dd2f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 18 Jan 2019 11:42:45 -0500 Subject: [PATCH 4/6] DRY-up hover-label alignment tests --- test/jasmine/tests/hover_label_test.js | 231 ++++++++++++------------- 1 file changed, 111 insertions(+), 120 deletions(-) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 2e85fa0016e..e9b43e13498 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1432,143 +1432,134 @@ describe('hover info', function() { .catch(failTest) .then(done); }); - }); - function hoverInfoNodes(traceName) { - var g = d3.selectAll('g.hoverlayer g.hovertext').filter(function() { - return !d3.select(this).select('[data-unformatted="' + traceName + '"]').empty(); - }); - - return { - primaryText: g.select('text:not([data-unformatted="' + traceName + '"])').node(), - primaryBox: g.select('path').node(), - secondaryText: g.select('[data-unformatted="' + traceName + '"]').node(), - secondaryBox: g.select('rect').node() - }; - } - - function ensureCentered(hoverInfoNodes) { - expect(hoverInfoNodes.primaryText.getAttribute('text-anchor')).toBe('middle'); - expect(hoverInfoNodes.secondaryText.getAttribute('text-anchor')).toBe('middle'); - return hoverInfoNodes; - } - - function assertLabelsInsideBoxes(nodes, msgPrefix) { - var msgPrefixFmt = msgPrefix ? '[' + msgPrefix + '] ' : ''; - - assertElemInside(nodes.primaryText, nodes.primaryBox, - msgPrefixFmt + 'Primary text inside box'); - assertElemInside(nodes.secondaryText, nodes.secondaryBox, - msgPrefixFmt + 'Secondary text inside box'); - } - - function assertSecondaryRightToPrimaryBox(nodes, msgPrefix) { - var msgPrefixFmt = msgPrefix ? '[' + msgPrefix + '] ' : ''; - - assertElemRightTo(nodes.secondaryBox, nodes.primaryBox, - msgPrefixFmt + 'Secondary box right to primary box'); - assertElemTopsAligned(nodes.secondaryBox, nodes.primaryBox, - msgPrefixFmt + 'Top edges of primary and secondary boxes aligned'); - } - - describe('centered', function() { - var trace1 = { - x: ['giraffes'], - y: [5], - name: 'LA Zoo', - type: 'bar', - text: ['Way too long hover info!'] - }; - var trace2 = { - x: ['giraffes'], - y: [5], - name: 'SF Zoo', - type: 'bar', - text: ['San Francisco'] - }; - var data = [trace1, trace2]; - var layout = {width: 600, height: 300, barmode: 'stack'}; - + describe('alignment while avoiding overlaps:', function() { var gd; - beforeEach(function(done) { - gd = createGraphDiv(); - Plotly.plot(gd, data, layout).then(done); - }); + beforeEach(function() { gd = createGraphDiv(); }); - it('renders labels inside boxes', function() { - _hover(gd, 300, 150); + function hoverInfoNodes(traceName) { + var g = d3.selectAll('g.hoverlayer g.hovertext').filter(function() { + return !d3.select(this).select('[data-unformatted="' + traceName + '"]').empty(); + }); - var nodes = ensureCentered(hoverInfoNodes('LA Zoo')); - assertLabelsInsideBoxes(nodes); - }); + return { + primaryText: g.select('text:not([data-unformatted="' + traceName + '"])').node(), + primaryBox: g.select('path').node(), + secondaryText: g.select('[data-unformatted="' + traceName + '"]').node(), + secondaryBox: g.select('rect').node() + }; + } - it('renders secondary info box right to primary info box', function() { - _hover(gd, 300, 150); + function ensureCentered(hoverInfoNodes) { + expect(hoverInfoNodes.primaryText.getAttribute('text-anchor')).toBe('middle'); + expect(hoverInfoNodes.secondaryText.getAttribute('text-anchor')).toBe('middle'); + return hoverInfoNodes; + } - var nodes = ensureCentered(hoverInfoNodes('LA Zoo')); - assertSecondaryRightToPrimaryBox(nodes); - }); - }); + function assertLabelsInsideBoxes(nodes, msgPrefix) { + var msgPrefixFmt = msgPrefix ? '[' + msgPrefix + '] ' : ''; - describe('centered', function() { - var trace1 = { - x: ['giraffes'], - y: [5], - name: 'LA Zoo', - type: 'bar', - text: ['Way too long hover info!'] - }; - var trace2 = { - x: ['giraffes'], - y: [5], - name: 'SF Zoo', - type: 'bar', - text: ['Way too looooong hover info!'] - }; - var trace3 = { - x: ['giraffes'], - y: [5], - name: 'NY Zoo', - type: 'bar', - text: ['New York'] - }; - var data = [trace1, trace2, trace3]; - var layout = {width: 600, height: 300}; + assertElemInside(nodes.primaryText, nodes.primaryBox, + msgPrefixFmt + 'Primary text inside box'); + assertElemInside(nodes.secondaryText, nodes.secondaryBox, + msgPrefixFmt + 'Secondary text inside box'); + } - var gd; + function assertSecondaryRightToPrimaryBox(nodes, msgPrefix) { + var msgPrefixFmt = msgPrefix ? '[' + msgPrefix + '] ' : ''; - beforeEach(function(done) { - gd = createGraphDiv(); - Plotly.plot(gd, data, layout).then(done); - }); + assertElemRightTo(nodes.secondaryBox, nodes.primaryBox, + msgPrefixFmt + 'Secondary box right to primary box'); + assertElemTopsAligned(nodes.secondaryBox, nodes.primaryBox, + msgPrefixFmt + 'Top edges of primary and secondary boxes aligned'); + } - function calcLineOverlap(minA, maxA, minB, maxB) { - expect(minA).toBeLessThan(maxA); - expect(minB).toBeLessThan(maxB); + it('centered-aligned, should render labels inside boxes', function(done) { + var trace1 = { + x: ['giraffes'], + y: [5], + name: 'LA Zoo', + type: 'bar', + text: ['Way too long hover info!'] + }; + var trace2 = { + x: ['giraffes'], + y: [5], + name: 'SF Zoo', + type: 'bar', + text: ['San Francisco'] + }; + var data = [trace1, trace2]; + var layout = {width: 600, height: 300, barmode: 'stack'}; - var overlap = Math.min(maxA, maxB) - Math.max(minA, minB); - return Math.max(0, overlap); - } + Plotly.plot(gd, data, layout) + .then(function() { _hover(gd, 300, 150); }) + .then(function() { + var nodes = ensureCentered(hoverInfoNodes('LA Zoo')); + assertLabelsInsideBoxes(nodes); + assertSecondaryRightToPrimaryBox(nodes); + }) + .catch(failTest) + .then(done); + }); - it('stacks nicely upon each other', function() { - _hover(gd, 300, 150); + it('centered-aligned, should stack nicely upon each other', function(done) { + function calcLineOverlap(minA, maxA, minB, maxB) { + expect(minA).toBeLessThan(maxA); + expect(minB).toBeLessThan(maxB); - var nodesLA = ensureCentered(hoverInfoNodes('LA Zoo')); - var nodesSF = ensureCentered(hoverInfoNodes('SF Zoo')); + var overlap = Math.min(maxA, maxB) - Math.max(minA, minB); + return Math.max(0, overlap); + } - // Ensure layout correct - assertLabelsInsideBoxes(nodesLA, 'LA Zoo'); - assertLabelsInsideBoxes(nodesSF, 'SF Zoo'); - assertSecondaryRightToPrimaryBox(nodesLA, 'LA Zoo'); - assertSecondaryRightToPrimaryBox(nodesSF, 'SF Zoo'); + var trace1 = { + x: ['giraffes'], + y: [5], + name: 'LA Zoo', + type: 'bar', + text: ['Way too long hover info!'] + }; + var trace2 = { + x: ['giraffes'], + y: [5], + name: 'SF Zoo', + type: 'bar', + text: ['Way too looooong hover info!'] + }; + var trace3 = { + x: ['giraffes'], + y: [5], + name: 'NY Zoo', + type: 'bar', + text: ['New York'] + }; + var data = [trace1, trace2, trace3]; + var layout = {width: 600, height: 300}; - // Ensure stacking, finally - var boxLABB = nodesLA.primaryBox.getBoundingClientRect(); - var boxSFBB = nodesSF.primaryBox.getBoundingClientRect(); - expect(calcLineOverlap(boxLABB.top, boxLABB.bottom, boxSFBB.top, boxSFBB.bottom)) - .toBeWithin(0, 1); // Be robust against floating point arithmetic and subtle future layout changes + Plotly.plot(gd, data, layout) + .then(function() { _hover(gd, 300, 150); }) + .then(function() { + var nodesLA = ensureCentered(hoverInfoNodes('LA Zoo')); + var nodesSF = ensureCentered(hoverInfoNodes('SF Zoo')); + + // Ensure layout correct + assertLabelsInsideBoxes(nodesLA, 'LA Zoo'); + assertLabelsInsideBoxes(nodesSF, 'SF Zoo'); + assertSecondaryRightToPrimaryBox(nodesLA, 'LA Zoo'); + assertSecondaryRightToPrimaryBox(nodesSF, 'SF Zoo'); + + // Ensure stacking, finally + var boxLABB = nodesLA.primaryBox.getBoundingClientRect(); + var boxSFBB = nodesSF.primaryBox.getBoundingClientRect(); + + // Be robust against floating point arithmetic and subtle future layout changes + expect(calcLineOverlap(boxLABB.top, boxLABB.bottom, boxSFBB.top, boxSFBB.bottom)) + .toBeWithin(0, 1); + }) + .catch(failTest) + .then(done); }); }); From 3d937d01e6f147371648461c1630b5949e574a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 18 Jan 2019 12:25:10 -0500 Subject: [PATCH 5/6] fix #3442 - consider name label height in hoverlabel y-span --- src/components/fx/hover.js | 21 +++++---- test/jasmine/tests/hover_label_test.js | 60 ++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index d49265dee30..b8cdb97a1e3 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -990,6 +990,7 @@ function createHoverText(hoverData, opts, gd) { var tx2 = g.select('text.name'); var tx2width = 0; + var tx2height = 0; // secondary label for non-empty 'name' if(name && name !== text) { @@ -1001,18 +1002,20 @@ function createHoverText(hoverData, opts, gd) { .attr('data-notex', 1) .call(svgTextUtils.positionText, 0, 0) .call(svgTextUtils.convertToTspans, gd); - tx2width = tx2.node().getBoundingClientRect().width + 2 * HOVERTEXTPAD; - } - else { + + var t2bb = tx2.node().getBoundingClientRect(); + tx2width = t2bb.width + 2 * HOVERTEXTPAD; + tx2height = t2bb.height + 2 * HOVERTEXTPAD; + } else { tx2.remove(); g.select('rect').remove(); } - g.select('path') - .style({ - fill: numsColor, - stroke: contrastColor - }); + g.select('path').style({ + fill: numsColor, + stroke: contrastColor + }); + var tbb = tx.node().getBoundingClientRect(); var htx = d.xa._offset + (d.x0 + d.x1) / 2; var hty = d.ya._offset + (d.y0 + d.y1) / 2; @@ -1023,7 +1026,7 @@ function createHoverText(hoverData, opts, gd) { d.ty0 = outerTop - tbb.top; d.bx = tbb.width + 2 * HOVERTEXTPAD; - d.by = tbb.height + 2 * HOVERTEXTPAD; + d.by = Math.max(tbb.height + 2 * HOVERTEXTPAD, tx2height); d.anchor = 'start'; d.txwidth = tbb.width; d.tx2width = tx2width; diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index e9b43e13498..5d025b8c1d8 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1476,6 +1476,14 @@ describe('hover info', function() { msgPrefixFmt + 'Top edges of primary and secondary boxes aligned'); } + function calcLineOverlap(minA, maxA, minB, maxB) { + expect(minA).toBeLessThan(maxA); + expect(minB).toBeLessThan(maxB); + + var overlap = Math.min(maxA, maxB) - Math.max(minA, minB); + return Math.max(0, overlap); + } + it('centered-aligned, should render labels inside boxes', function(done) { var trace1 = { x: ['giraffes'], @@ -1506,14 +1514,6 @@ describe('hover info', function() { }); it('centered-aligned, should stack nicely upon each other', function(done) { - function calcLineOverlap(minA, maxA, minB, maxB) { - expect(minA).toBeLessThan(maxA); - expect(minB).toBeLessThan(maxB); - - var overlap = Math.min(maxA, maxB) - Math.max(minA, minB); - return Math.max(0, overlap); - } - var trace1 = { x: ['giraffes'], y: [5], @@ -1561,6 +1561,50 @@ describe('hover info', function() { .catch(failTest) .then(done); }); + + it('should stack multi-line trace-name labels nicely', function(done) { + var name = 'Multi
line
trace
name'; + var name2 = 'Multi
line
trace
name2'; + + Plotly.plot(gd, [{ + y: [1, 2, 1], + name: name, + hoverlabel: {namelength: -1}, + hoverinfo: 'x+y+name' + }, { + y: [1, 2, 1], + name: name2, + hoverinfo: 'x+y+name', + hoverlabel: {namelength: -1} + }], { + width: 600, + height: 300 + }) + .then(function() { _hoverNatural(gd, 209, 12); }) + .then(function() { + var nodes = hoverInfoNodes(name); + var nodes2 = hoverInfoNodes(name2); + + assertLabelsInsideBoxes(nodes, 'trace 0'); + assertLabelsInsideBoxes(nodes2, 'trace 2'); + assertSecondaryRightToPrimaryBox(nodes, 'trace 0'); + assertSecondaryRightToPrimaryBox(nodes2, 'trace 2'); + + var primaryBB = nodes.primaryBox.getBoundingClientRect(); + var primaryBB2 = nodes2.primaryBox.getBoundingClientRect(); + expect(calcLineOverlap(primaryBB.top, primaryBB.bottom, primaryBB2.top, primaryBB2.bottom)) + .toBeWithin(0, 1); + + // there's a bit of a gap in the secondary as they do have + // a border (for now) + var secondaryBB = nodes.secondaryBox.getBoundingClientRect(); + var secondaryBB2 = nodes2.secondaryBox.getBoundingClientRect(); + expect(calcLineOverlap(secondaryBB.top, secondaryBB.bottom, secondaryBB2.top, secondaryBB2.bottom)) + .toBeWithin(2, 1); + }) + .catch(failTest) + .then(done); + }); }); describe('hovertemplate', function() { From 6c7b0ede2e860ca9b7e03d4a849a4dedbccf2770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 21 Jan 2019 10:08:55 -0500 Subject: [PATCH 6/6] add support for , and in "name" hover labels --- src/components/fx/hover.js | 2 +- test/jasmine/tests/svg_text_utils_test.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index b8cdb97a1e3..b8b3f528470 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -927,7 +927,7 @@ function createHoverText(hoverData, opts, gd) { if(d.name) { name = svgTextUtils.plainText(d.name || '', { len: d.nameLength, - allowedTags: ['br', 'sub', 'sup'] + allowedTags: ['br', 'sub', 'sup', 'b', 'i', 'em'] }); } diff --git a/test/jasmine/tests/svg_text_utils_test.js b/test/jasmine/tests/svg_text_utils_test.js index 6610f189ccd..e6e41b201f4 100644 --- a/test/jasmine/tests/svg_text_utils_test.js +++ b/test/jasmine/tests/svg_text_utils_test.js @@ -504,5 +504,13 @@ describe('svg+text utils', function() { expect(fn(sIn, {allowedTags: ['sup']})).toBe('ThisIsDATA300'); expect(fn(sIn, {allowedTags: ['br', 'sup']})).toBe('ThisIs
DATA300'); }); + + it('should work in various cases w/ , and ', function() { + var sIn = 'ThisIsDATA300'; + + expect(fn(sIn)).toBe('ThisIsDATA300'); + expect(fn(sIn, {allowedTags: ['i', 'b', 'em']})).toBe('ThisIsDATA300'); + expect(fn(sIn, {len: 10, allowedTags: ['i', 'b', 'em']})).toBe('ThisIsD...'); + }); }); });