From f1d497ec7069c24a933a06bc6acbd57d3e5d4b13 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Tue, 6 Aug 2019 10:47:32 -0700 Subject: [PATCH] feat: optimize functions for getting text dimension (#199) * feat: add function for getting multiple text dimensions * feat: lazy deletion * feat: use lazy factory * fix: comments * fix: rename variable --- .../src/getMultipleTextDimensions.ts | 66 ++++++ .../src/getTextDimension.ts | 66 +++--- .../superset-ui-dimension/src/index.ts | 1 + .../src/svg/LazyFactory.ts | 41 ++++ .../src/svg/constants.ts | 2 + .../src/svg/createHiddenSvgNode.ts | 10 + .../src/svg/createTextNode.ts | 5 + .../src/svg/factories.ts | 6 + .../src/svg/getBBoxCeil.ts | 15 ++ .../src/svg/updateTextNode.ts | 47 ++++ .../test/computeMaxFontSize.test.ts | 16 +- .../{addDummyFill.ts => getBBoxDummyFill.ts} | 26 ++- .../test/getMultipleTextDimensions.test.ts | 205 ++++++++++++++++++ .../test/getTextDimension.test.ts | 58 +++-- .../test/svg/LazyFactory.test.ts | 49 +++++ .../test/svg/getBBoxCeil.test.ts | 39 ++++ .../test/svg/updateTextNode.test.ts | 74 +++++++ 17 files changed, 646 insertions(+), 80 deletions(-) create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/getMultipleTextDimensions.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/LazyFactory.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/constants.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/createHiddenSvgNode.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/createTextNode.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/factories.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/getBBoxCeil.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/updateTextNode.ts rename superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/{addDummyFill.ts => getBBoxDummyFill.ts} (61%) create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/getMultipleTextDimensions.test.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/LazyFactory.test.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/getBBoxCeil.test.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/updateTextNode.test.ts diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/getMultipleTextDimensions.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/getMultipleTextDimensions.ts new file mode 100644 index 000000000000..ae5aa9163bd2 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/getMultipleTextDimensions.ts @@ -0,0 +1,66 @@ +import { TextStyle, Dimension } from './types'; +import getBBoxCeil from './svg/getBBoxCeil'; +import { hiddenSvgFactory, textFactory } from './svg/factories'; +import updateTextNode from './svg/updateTextNode'; + +/** + * get dimensions of multiple texts with same style + * @param input + * @param defaultDimension + */ +export default function getMultipleTextDimensions( + input: { + className?: string; + container?: HTMLElement; + style?: TextStyle; + texts: string[]; + }, + defaultDimension?: Dimension, +): Dimension[] { + const { texts, className, style, container } = input; + + const cache = new Map(); + // for empty string + cache.set('', { height: 0, width: 0 }); + let textNode: SVGTextElement | undefined; + let svgNode: SVGSVGElement | undefined; + + const dimensions = texts.map(text => { + // Check if this string has been computed already + if (cache.has(text)) { + return cache.get(text) as Dimension; + } + + // Lazy creation of text and svg nodes + if (!textNode) { + svgNode = hiddenSvgFactory.createInContainer(container); + textNode = textFactory.createInContainer(svgNode); + } + + // Update text and get dimension + updateTextNode(textNode, { className, style, text }); + const dimension = getBBoxCeil(textNode, defaultDimension); + // Store result to cache + cache.set(text, dimension); + + return dimension; + }); + + // Remove svg node, if any + if (svgNode && textNode) { + // The nodes are added to the DOM briefly only to make getBBox works. + // (If not added to DOM getBBox will always return 0x0.) + // After that the svg nodes are not needed. + // We delay its removal in case there are subsequent calls to this function + // that can reuse the svg nodes. + // Experiments have shown that reusing existing nodes + // instead of deleting and adding new ones can save lot of time. + setTimeout(() => { + textFactory.removeFromContainer(svgNode); + hiddenSvgFactory.removeFromContainer(container); + // eslint-disable-next-line no-magic-numbers + }, 500); + } + + return dimensions; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/getTextDimension.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/getTextDimension.ts index f3181fb00c9c..00c0d61235f4 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/getTextDimension.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/getTextDimension.ts @@ -1,14 +1,7 @@ import { TextStyle, Dimension } from './types'; - -const SVG_NS = 'http://www.w3.org/2000/svg'; -const STYLE_FIELDS: (keyof TextStyle)[] = [ - 'font', - 'fontWeight', - 'fontStyle', - 'fontSize', - 'fontFamily', - 'letterSpacing', -]; +import updateTextNode from './svg/updateTextNode'; +import getBBoxCeil from './svg/getBBoxCeil'; +import { hiddenSvgFactory, textFactory } from './svg/factories'; export interface GetTextDimensionInput { className?: string; @@ -17,39 +10,34 @@ export interface GetTextDimensionInput { text: string; } -const DEFAULT_DIMENSION = { height: 20, width: 100 }; - export default function getTextDimension( input: GetTextDimensionInput, - defaultDimension: Dimension = DEFAULT_DIMENSION, + defaultDimension?: Dimension, ): Dimension { - const { text, className, style = {}, container = document.body } = input; - - const textNode = document.createElementNS(SVG_NS, 'text'); - textNode.textContent = text; + const { text, className, style, container } = input; - if (className !== undefined && className !== null) { - textNode.setAttribute('class', className); + // Empty string + if (text.length === 0) { + return { height: 0, width: 0 }; } - STYLE_FIELDS.filter( - (field: keyof TextStyle) => style[field] !== undefined && style[field] !== null, - ).forEach((field: keyof TextStyle) => { - textNode.style[field] = `${style[field]}`; - }); - - const svg = document.createElementNS(SVG_NS, 'svg'); - svg.style.position = 'absolute'; // so it won't disrupt page layout - svg.style.opacity = '0'; // and not visible - svg.style.pointerEvents = 'none'; // and not capturing mouse events - svg.appendChild(textNode); - container.appendChild(svg); - - const bbox = textNode.getBBox ? textNode.getBBox() : defaultDimension; - container.removeChild(svg); - - return { - height: Math.ceil(bbox.height), - width: Math.ceil(bbox.width), - }; + const svgNode = hiddenSvgFactory.createInContainer(container); + const textNode = textFactory.createInContainer(svgNode); + updateTextNode(textNode, { className, style, text }); + const dimension = getBBoxCeil(textNode, defaultDimension); + + // The nodes are added to the DOM briefly only to make getBBox works. + // (If not added to DOM getBBox will always return 0x0.) + // After that the svg nodes are not needed. + // We delay its removal in case there are subsequent calls to this function + // that can reuse the svg nodes. + // Experiments have shown that reusing existing nodes + // instead of deleting and adding new ones can save lot of time. + setTimeout(() => { + textFactory.removeFromContainer(svgNode); + hiddenSvgFactory.removeFromContainer(container); + // eslint-disable-next-line no-magic-numbers + }, 500); + + return dimension; } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/index.ts index 496b224c0d31..8f0de0ccfb7d 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/index.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/index.ts @@ -1,4 +1,5 @@ export { default as getTextDimension } from './getTextDimension'; +export { default as getMultipleTextDimensions } from './getMultipleTextDimensions'; export { default as computeMaxFontSize } from './computeMaxFontSize'; export { default as mergeMargin } from './mergeMargin'; export { default as parseLength } from './parseLength'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/LazyFactory.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/LazyFactory.ts new file mode 100644 index 000000000000..8153e018998f --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/LazyFactory.ts @@ -0,0 +1,41 @@ +export default class LazyFactory { + private activeNodes = new Map< + HTMLElement | SVGElement, + { + counter: number; + node: T; + } + >(); + + private factoryFn: () => T; + + constructor(factoryFn: () => T) { + this.factoryFn = factoryFn; + } + + createInContainer(container: HTMLElement | SVGElement = document.body) { + if (this.activeNodes.has(container)) { + const entry = this.activeNodes.get(container)!; + entry.counter += 1; + + return entry.node; + } + + const node = this.factoryFn(); + container.appendChild(node); + this.activeNodes.set(container, { counter: 1, node }); + + return node; + } + + removeFromContainer(container: HTMLElement | SVGElement = document.body) { + if (this.activeNodes.has(container)) { + const entry = this.activeNodes.get(container)!; + entry.counter -= 1; + if (entry.counter === 0) { + container.removeChild(entry.node); + this.activeNodes.delete(container); + } + } + } +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/constants.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/constants.ts new file mode 100644 index 000000000000..1873033670e4 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/constants.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const SVG_NS = 'http://www.w3.org/2000/svg'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/createHiddenSvgNode.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/createHiddenSvgNode.ts new file mode 100644 index 000000000000..82a180ddc17e --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/createHiddenSvgNode.ts @@ -0,0 +1,10 @@ +import { SVG_NS } from './constants'; + +export default function createHiddenSvgNode() { + const svgNode = document.createElementNS(SVG_NS, 'svg'); + svgNode.style.position = 'absolute'; // so it won't disrupt page layout + svgNode.style.opacity = '0'; // and not visible + svgNode.style.pointerEvents = 'none'; // and not capturing mouse events + + return svgNode; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/createTextNode.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/createTextNode.ts new file mode 100644 index 000000000000..c831f0dc9ec5 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/createTextNode.ts @@ -0,0 +1,5 @@ +import { SVG_NS } from './constants'; + +export default function createTextNode() { + return document.createElementNS(SVG_NS, 'text'); +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/factories.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/factories.ts new file mode 100644 index 000000000000..522efb4d388d --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/factories.ts @@ -0,0 +1,6 @@ +import LazyFactory from './LazyFactory'; +import createHiddenSvgNode from './createHiddenSvgNode'; +import createTextNode from './createTextNode'; + +export const hiddenSvgFactory = new LazyFactory(createHiddenSvgNode); +export const textFactory = new LazyFactory(createTextNode); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/getBBoxCeil.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/getBBoxCeil.ts new file mode 100644 index 000000000000..87e93a5a93a1 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/getBBoxCeil.ts @@ -0,0 +1,15 @@ +import { Dimension } from '../types'; + +const DEFAULT_DIMENSION = { height: 20, width: 100 }; + +export default function getBBoxCeil( + node: SVGGraphicsElement, + defaultDimension: Dimension = DEFAULT_DIMENSION, +) { + const { width, height } = node.getBBox ? node.getBBox() : defaultDimension; + + return { + height: Math.ceil(height), + width: Math.ceil(width), + }; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/updateTextNode.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/updateTextNode.ts new file mode 100644 index 000000000000..4dea4791c168 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/src/svg/updateTextNode.ts @@ -0,0 +1,47 @@ +import { TextStyle } from '../types'; + +const STYLE_FIELDS: (keyof TextStyle)[] = [ + 'font', + 'fontWeight', + 'fontStyle', + 'fontSize', + 'fontFamily', + 'letterSpacing', +]; + +export default function updateTextNode( + node: SVGTextElement, + { + className, + style = {}, + text, + }: { + className?: string; + style?: TextStyle; + text?: string; + } = {}, +) { + const textNode = node; + + if (textNode.textContent !== text) { + textNode.textContent = typeof text === 'undefined' ? null : text; + } + if (textNode.getAttribute('class') !== className) { + textNode.setAttribute('class', className || ''); + } + + // clear style + STYLE_FIELDS.forEach((field: keyof TextStyle) => { + textNode.style[field] = null; + }); + + // apply new style + // Note that the font field will auto-populate other font fields when applicable. + STYLE_FIELDS.filter( + (field: keyof TextStyle) => typeof style[field] !== 'undefined' && style[field] !== null, + ).forEach((field: keyof TextStyle) => { + textNode.style[field] = `${style[field]}`; + }); + + return textNode; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/computeMaxFontSize.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/computeMaxFontSize.test.ts index 559479d93716..6ebf06e78d40 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/computeMaxFontSize.test.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/computeMaxFontSize.test.ts @@ -1,20 +1,10 @@ import { computeMaxFontSize } from '../src/index'; -import addDummyFill from './addDummyFill'; +import { addDummyFill, removeDummyFill } from './getBBoxDummyFill'; describe('computeMaxFontSize(input)', () => { describe('returns dimension of the given text', () => { - let originalFn: () => DOMRect; - - beforeEach(() => { - // @ts-ignore - fix jsdom - originalFn = SVGElement.prototype.getBBox; - addDummyFill(); - }); - - afterEach(() => { - // @ts-ignore - fix jsdom - SVGElement.prototype.getBBox = originalFn; - }); + beforeEach(addDummyFill); + afterEach(removeDummyFill); it('requires either idealFontSize or maxHeight', () => { expect(() => diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/addDummyFill.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/getBBoxDummyFill.ts similarity index 61% rename from superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/addDummyFill.ts rename to superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/getBBoxDummyFill.ts index 4def23b2b800..56f07a94a481 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/addDummyFill.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/getBBoxDummyFill.ts @@ -1,17 +1,28 @@ -export const SAMPLE_TEXT = 'dummy text. does not really matter'; +let originalFn: () => DOMRect; + +const textToWidth = { + paris: 200, + tokyo: 300, + beijing: 400, +}; + +export const SAMPLE_TEXT = Object.keys(textToWidth); + +export function addDummyFill() { + // @ts-ignore - fix jsdom + originalFn = SVGElement.prototype.getBBox; -export default function addDummyFill() { // @ts-ignore - fix jsdom SVGElement.prototype.getBBox = function getBBox() { - let width = 200; + let width = textToWidth[this.textContent] || 200; let height = 20; if (this.getAttribute('class') === 'test-class') { - width = 100; + width /= 2; } if (this.style.fontFamily === 'Lobster') { - width = 250; + width *= 1.25; } if (this.style.fontSize) { @@ -45,3 +56,8 @@ export default function addDummyFill() { }; }; } + +export function removeDummyFill() { + // @ts-ignore - fix jsdom + SVGElement.prototype.getBBox = originalFn; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/getMultipleTextDimensions.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/getMultipleTextDimensions.test.ts new file mode 100644 index 000000000000..78a6c7e1879d --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/getMultipleTextDimensions.test.ts @@ -0,0 +1,205 @@ +import { getMultipleTextDimensions } from '../src/index'; +import { addDummyFill, removeDummyFill, SAMPLE_TEXT } from './getBBoxDummyFill'; +import promiseTimeout from '../../superset-ui-chart/test/components/promiseTimeout'; + +describe('getTextDimension(input)', () => { + describe('returns dimension of the given text', () => { + beforeEach(addDummyFill); + afterEach(removeDummyFill); + + it('takes an array of text as argument', () => { + expect( + getMultipleTextDimensions({ + texts: [SAMPLE_TEXT[0], SAMPLE_TEXT[1], ''], + }), + ).toEqual([ + { + height: 20, + width: 200, + }, + { + height: 20, + width: 300, + }, + { + height: 0, + width: 0, + }, + ]); + }); + it('handles empty text', () => { + expect( + getMultipleTextDimensions({ + texts: ['', ''], + }), + ).toEqual([ + { + height: 0, + width: 0, + }, + { + height: 0, + width: 0, + }, + ]); + }); + it('handles duplicate text', () => { + expect( + getMultipleTextDimensions({ + texts: [SAMPLE_TEXT[0], SAMPLE_TEXT[0]], + }), + ).toEqual([ + { + height: 20, + width: 200, + }, + { + height: 20, + width: 200, + }, + ]); + }); + it('accepts provided class via className', () => { + expect( + getMultipleTextDimensions({ + texts: [SAMPLE_TEXT[0], SAMPLE_TEXT[1]], + className: 'test-class', + }), + ).toEqual([ + { + height: 20, + width: 100, + }, + { + height: 20, + width: 150, + }, + ]); + }); + it('accepts provided style.font', () => { + expect( + getMultipleTextDimensions({ + texts: [SAMPLE_TEXT[0], SAMPLE_TEXT[1]], + style: { + font: 'italic 700 30px Lobster', + }, + }), + ).toEqual([ + { + height: 30, // 20 * (30/20) [fontSize=30] + width: 1125, // 200 * 1.25 [fontFamily=Lobster] * (30/20) [fontSize=30] * 1.5 [fontStyle=italic] * 2 [fontWeight=700] + }, + { + height: 30, + width: 1688, // 300 * 1.25 [fontFamily=Lobster] * (30/20) [fontSize=30] * 1.5 [fontStyle=italic] * 2 [fontWeight=700] + }, + ]); + }); + it('accepts provided style.fontFamily', () => { + expect( + getMultipleTextDimensions({ + texts: [SAMPLE_TEXT[0], SAMPLE_TEXT[1]], + style: { + fontFamily: 'Lobster', + }, + }), + ).toEqual([ + { + height: 20, + width: 250, // 200 * 1.25 [fontFamily=Lobster] + }, + { + height: 20, + width: 375, // 300 * 1.25 [fontFamily=Lobster] + }, + ]); + }); + it('accepts provided style.fontSize', () => { + expect( + getMultipleTextDimensions({ + texts: [SAMPLE_TEXT[0], SAMPLE_TEXT[1]], + style: { + fontSize: '40px', + }, + }), + ).toEqual([ + { + height: 40, // 20 [baseHeight] * (40/20) [fontSize=40] + width: 400, // 200 [baseWidth] * (40/20) [fontSize=40] + }, + { + height: 40, + width: 600, // 300 [baseWidth] * (40/20) [fontSize=40] + }, + ]); + }); + it('accepts provided style.fontStyle', () => { + expect( + getMultipleTextDimensions({ + texts: [SAMPLE_TEXT[0], SAMPLE_TEXT[1]], + style: { + fontStyle: 'italic', + }, + }), + ).toEqual([ + { + height: 20, + width: 300, // 200 [baseWidth] * 1.5 [fontStyle=italic] + }, + { + height: 20, + width: 450, // 300 [baseWidth] * 1.5 [fontStyle=italic] + }, + ]); + }); + it('accepts provided style.fontWeight', () => { + expect( + getMultipleTextDimensions({ + texts: [SAMPLE_TEXT[0], SAMPLE_TEXT[1]], + style: { + fontWeight: 700, + }, + }), + ).toEqual([ + { + height: 20, + width: 400, // 200 [baseWidth] * 2 [fontWeight=700] + }, + { + height: 20, + width: 600, // 300 [baseWidth] * 2 [fontWeight=700] + }, + ]); + }); + it('accepts provided style.letterSpacing', () => { + expect( + getMultipleTextDimensions({ + texts: [SAMPLE_TEXT[0], SAMPLE_TEXT[1]], + style: { + letterSpacing: '1.1', + }, + }), + ).toEqual([ + { + height: 20, + width: 221, // Ceiling(200 [baseWidth] * 1.1 [letterSpacing=1.1]) + }, + { + height: 20, + width: 330, // Ceiling(300 [baseWidth] * 1.1 [letterSpacing=1.1]) + }, + ]); + }); + }); + it('cleans up DOM', () => { + getMultipleTextDimensions({ + texts: [SAMPLE_TEXT[0], SAMPLE_TEXT[1]], + }); + + expect(document.querySelectorAll('svg')).toHaveLength(1); + + return promiseTimeout(() => { + expect(document.querySelector('svg')).toBeNull(); + }, 600); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/getTextDimension.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/getTextDimension.test.ts index 2eb36f46ba9d..47a57265953d 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/getTextDimension.test.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/getTextDimension.test.ts @@ -1,12 +1,13 @@ import { getTextDimension } from '../src/index'; -import addDummyFill, { SAMPLE_TEXT } from './addDummyFill'; +import { addDummyFill, removeDummyFill, SAMPLE_TEXT } from './getBBoxDummyFill'; +import promiseTimeout from '../../superset-ui-chart/test/components/promiseTimeout'; describe('getTextDimension(input)', () => { describe('returns default dimension if getBBox() is not available', () => { it('returns default value for default dimension', () => { expect( getTextDimension({ - text: SAMPLE_TEXT, + text: SAMPLE_TEXT[0], }), ).toEqual({ height: 20, @@ -17,7 +18,7 @@ describe('getTextDimension(input)', () => { expect( getTextDimension( { - text: SAMPLE_TEXT, + text: SAMPLE_TEXT[0], }, { height: 30, @@ -31,23 +32,13 @@ describe('getTextDimension(input)', () => { }); }); describe('returns dimension of the given text', () => { - let originalFn: () => DOMRect; - - beforeEach(() => { - // @ts-ignore - fix jsdom - originalFn = SVGElement.prototype.getBBox; - addDummyFill(); - }); - - afterEach(() => { - // @ts-ignore - fix jsdom - SVGElement.prototype.getBBox = originalFn; - }); + beforeEach(addDummyFill); + afterEach(removeDummyFill); it('takes text as argument', () => { expect( getTextDimension({ - text: SAMPLE_TEXT, + text: SAMPLE_TEXT[0], }), ).toEqual({ height: 20, @@ -57,7 +48,7 @@ describe('getTextDimension(input)', () => { it('accepts provided class via className', () => { expect( getTextDimension({ - text: SAMPLE_TEXT, + text: SAMPLE_TEXT[0], className: 'test-class', }), ).toEqual({ @@ -68,7 +59,7 @@ describe('getTextDimension(input)', () => { it('accepts provided style.font', () => { expect( getTextDimension({ - text: SAMPLE_TEXT, + text: SAMPLE_TEXT[0], style: { font: 'italic 700 30px Lobster', }, @@ -81,7 +72,7 @@ describe('getTextDimension(input)', () => { it('accepts provided style.fontFamily', () => { expect( getTextDimension({ - text: SAMPLE_TEXT, + text: SAMPLE_TEXT[0], style: { fontFamily: 'Lobster', }, @@ -94,7 +85,7 @@ describe('getTextDimension(input)', () => { it('accepts provided style.fontSize', () => { expect( getTextDimension({ - text: SAMPLE_TEXT, + text: SAMPLE_TEXT[0], style: { fontSize: '40px', }, @@ -107,7 +98,7 @@ describe('getTextDimension(input)', () => { it('accepts provided style.fontStyle', () => { expect( getTextDimension({ - text: SAMPLE_TEXT, + text: SAMPLE_TEXT[0], style: { fontStyle: 'italic', }, @@ -120,7 +111,7 @@ describe('getTextDimension(input)', () => { it('accepts provided style.fontWeight', () => { expect( getTextDimension({ - text: SAMPLE_TEXT, + text: SAMPLE_TEXT[0], style: { fontWeight: 700, }, @@ -133,7 +124,7 @@ describe('getTextDimension(input)', () => { it('accepts provided style.letterSpacing', () => { expect( getTextDimension({ - text: SAMPLE_TEXT, + text: SAMPLE_TEXT[0], style: { letterSpacing: '1.1', }, @@ -143,5 +134,26 @@ describe('getTextDimension(input)', () => { width: 221, // Ceiling(200 [baseWidth] * 1.1 [letterSpacing=1.1]) }); }); + it('handle empty text', () => { + expect( + getTextDimension({ + text: '', + }), + ).toEqual({ + height: 0, + width: 0, + }); + }); + }); + it('cleans up DOM', () => { + getTextDimension({ + text: SAMPLE_TEXT[0], + }); + + expect(document.querySelectorAll('svg')).toHaveLength(1); + + return promiseTimeout(() => { + expect(document.querySelector('svg')).toBeNull(); + }, 600); }); }); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/LazyFactory.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/LazyFactory.test.ts new file mode 100644 index 000000000000..c26df9585785 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/LazyFactory.test.ts @@ -0,0 +1,49 @@ +import LazyFactory from '../../src/svg/LazyFactory'; + +describe('LazyFactory', () => { + describe('createInContainer()', () => { + it('creates node in the specified container', () => { + const factory = new LazyFactory(() => document.createElement('div')); + const div = factory.createInContainer(); + const innerDiv = factory.createInContainer(div); + expect(div.parentNode).toEqual(document.body); + expect(innerDiv.parentNode).toEqual(div); + }); + it('reuses existing', () => { + const factoryFn = jest.fn(() => document.createElement('div')); + const factory = new LazyFactory(factoryFn); + const div1 = factory.createInContainer(); + const div2 = factory.createInContainer(); + expect(div1).toBe(div2); + expect(factoryFn).toHaveBeenCalledTimes(1); + }); + }); + describe('removeFromContainer', () => { + it('removes node from container', () => { + const factory = new LazyFactory(() => document.createElement('div')); + const div = factory.createInContainer(); + const innerDiv = factory.createInContainer(div); + expect(div.parentNode).toEqual(document.body); + expect(innerDiv.parentNode).toEqual(div); + factory.removeFromContainer(); + factory.removeFromContainer(div); + expect(div.parentNode).toBeNull(); + expect(innerDiv.parentNode).toBeNull(); + }); + it('does not remove if others are using', () => { + const factory = new LazyFactory(() => document.createElement('div')); + const div1 = factory.createInContainer(); + factory.createInContainer(); + factory.removeFromContainer(); + expect(div1.parentNode).toEqual(document.body); + factory.removeFromContainer(); + expect(div1.parentNode).toBeNull(); + }); + it('does not crash if try to remove already removed node', () => { + const factory = new LazyFactory(() => document.createElement('div')); + factory.createInContainer(); + factory.removeFromContainer(); + expect(() => factory.removeFromContainer()).not.toThrow(); + }); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/getBBoxCeil.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/getBBoxCeil.test.ts new file mode 100644 index 000000000000..397c867dedb4 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/getBBoxCeil.test.ts @@ -0,0 +1,39 @@ +import getBBoxCeil from '../../src/svg/getBBoxCeil'; +import createTextNode from '../../src/svg/createTextNode'; + +describe('getBBoxCeil(node, defaultDimension)', () => { + describe('returns default dimension if getBBox() is not available', () => { + it('returns default value for default dimension', () => { + expect(getBBoxCeil(createTextNode())).toEqual({ + height: 20, + width: 100, + }); + }); + it('return specified value if specified', () => { + expect( + getBBoxCeil(createTextNode(), { + height: 30, + width: 400, + }), + ).toEqual({ + height: 30, + width: 400, + }); + }); + }); + describe('returns ceiling of the svg element', () => { + it('converts to ceiling if value is not integer', () => { + expect(getBBoxCeil(createTextNode(), { height: 10.6, width: 11.1 })).toEqual({ + height: 11, + width: 12, + }); + }); + + it('does nothing if value is integer', () => { + expect(getBBoxCeil(createTextNode())).toEqual({ + height: 20, + width: 100, + }); + }); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/updateTextNode.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/updateTextNode.test.ts new file mode 100644 index 000000000000..8d4dd47e1157 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-dimension/test/svg/updateTextNode.test.ts @@ -0,0 +1,74 @@ +import updateTextNode from '../../src/svg/updateTextNode'; +import createTextNode from '../../src/svg/createTextNode'; + +describe('updateTextNode(node, options)', () => { + it('handles empty options', () => { + const node = updateTextNode(createTextNode()); + expect(node.getAttribute('class')).toEqual(''); + expect(node.style.font).toEqual(''); + expect(node.style.fontWeight).toEqual(''); + expect(node.style.fontSize).toEqual(''); + expect(node.style.fontStyle).toEqual(''); + expect(node.style.fontFamily).toEqual(''); + expect(node.style.letterSpacing).toEqual(''); + expect(node.textContent).toEqual(''); + }); + + it('handles setting class', () => { + const node = updateTextNode(createTextNode(), { className: 'abc' }); + expect(node.getAttribute('class')).toEqual('abc'); + expect(node.style.font).toEqual(''); + expect(node.style.fontWeight).toEqual(''); + expect(node.style.fontSize).toEqual(''); + expect(node.style.fontStyle).toEqual(''); + expect(node.style.fontFamily).toEqual(''); + expect(node.style.letterSpacing).toEqual(''); + expect(node.textContent).toEqual(''); + }); + + it('handles setting text', () => { + const node = updateTextNode(createTextNode(), { text: 'abc' }); + expect(node.getAttribute('class')).toEqual(''); + expect(node.style.font).toEqual(''); + expect(node.style.fontWeight).toEqual(''); + expect(node.style.fontSize).toEqual(''); + expect(node.style.fontStyle).toEqual(''); + expect(node.style.fontFamily).toEqual(''); + expect(node.style.letterSpacing).toEqual(''); + expect(node.textContent).toEqual('abc'); + }); + + it('handles setting font', () => { + const node = updateTextNode(createTextNode(), { + style: { + font: 'italic 30px Lobster 700', + }, + }); + expect(node.getAttribute('class')).toEqual(''); + expect(node.style.fontWeight).toEqual('700'); + expect(node.style.fontSize).toEqual('30px'); + expect(node.style.fontStyle).toEqual('italic'); + expect(node.style.fontFamily).toEqual('Lobster'); + expect(node.style.letterSpacing).toEqual(''); + expect(node.textContent).toEqual(''); + }); + + it('handles setting specific font style', () => { + const node = updateTextNode(createTextNode(), { + style: { + fontFamily: 'Lobster', + fontStyle: 'italic', + fontWeight: '700', + fontSize: '30px', + letterSpacing: 1.1, + }, + }); + expect(node.getAttribute('class')).toEqual(''); + expect(node.style.fontWeight).toEqual('700'); + expect(node.style.fontSize).toEqual('30px'); + expect(node.style.fontStyle).toEqual('italic'); + expect(node.style.fontFamily).toEqual('Lobster'); + expect(node.style.letterSpacing).toEqual('1.1'); + expect(node.textContent).toEqual(''); + }); +});