Skip to content

Commit

Permalink
[EuiTextTruncate] Fix testenv mocks (#7234)
Browse files Browse the repository at this point in the history
  • Loading branch information
cee-chen authored Sep 29, 2023
1 parent 6c6891b commit 1fa8cf0
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 152 deletions.
10 changes: 6 additions & 4 deletions scripts/jest/setup/mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ jest.mock('./../../../src/components/icon', () => {
return { EuiIcon };
});

jest.mock('./../../../src/components/text_truncate', () => {
const rest = jest.requireActual('./../../../src/components/text_truncate');
const utils = require('./../../../src/components/text_truncate/utils.testenv');
return { ...rest, ...utils };
jest.mock('./../../../src/services/canvas', () => {
const rest = jest.requireActual('./../../../src/services/canvas');
const {
CanvasTextUtils,
} = require('./../../../src/services/canvas/canvas_text_utils.testenv');
return { ...rest, CanvasTextUtils };
});

jest.mock('./../../../src/services/accessibility', () => {
Expand Down
3 changes: 1 addition & 2 deletions src/components/combo_box/combo_box_input/combo_box_input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import React, {
import classNames from 'classnames';

import { CommonProps } from '../../common';
import { htmlIdGenerator, keys } from '../../../services';
import { CanvasTextUtils } from '../../text_truncate';
import { htmlIdGenerator, keys, CanvasTextUtils } from '../../../services';
import { EuiScreenReaderOnly } from '../../accessibility';
import {
EuiFormControlLayout,
Expand Down
2 changes: 1 addition & 1 deletion src/components/text_truncate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export type {
} from './text_truncate';
export { EuiTextTruncate } from './text_truncate';

export { CanvasTextUtils, TruncationUtils } from './utils';
export { TruncationUtils } from './utils';
65 changes: 15 additions & 50 deletions src/components/text_truncate/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,7 @@
* Side Public License, v 1.
*/

import { CanvasTextUtils, TruncationUtils } from './utils';

let mockCanvasWidth = 0;
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
value: () => ({ measureText: () => ({ width: mockCanvasWidth }), font: '' }),
});

describe('CanvasTextUtils', () => {
describe('font calculations', () => {
it('computes the set font if passed a container element', () => {
const container = document.createElement('div');
container.style.font = '14px Inter';

const utils = new CanvasTextUtils({ container });
expect(utils.context.font).toEqual('14px Inter');
});

it('accepts a static font string', () => {
const utils = new CanvasTextUtils({ font: '14px Inter' });
expect(utils.context.font).toEqual('14px Inter');
});
});

describe('text width utils', () => {
const utils = new CanvasTextUtils({ font: '' });

describe('textWidth', () => {
it('returns the measured text width from the canvas', () => {
mockCanvasWidth = 200;
expect(utils.textWidth).toEqual(200);
});
});

describe('setTextToCheck', () => {
it('sets the internal currentText variable', () => {
utils.setTextToCheck('hello world');
expect(utils.currentText).toEqual('hello world');
});
});
});
});
import { TruncationUtils } from './utils';

describe('TruncationUtils', () => {
const params = {
Expand All @@ -56,6 +16,11 @@ describe('TruncationUtils', () => {
font: '14px Inter',
};

const setMockTextWidth = (width: number) => (utils: TruncationUtils) => {
// @ts-ignore - mocked canvas_text_utils.testenv allows setting this value
utils.textWidth = width;
};

// A few utilities log errors - silence them and capture the messages
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
beforeEach(() => consoleErrorSpy.mockClear());
Expand All @@ -69,23 +34,23 @@ describe('TruncationUtils', () => {

describe('setTextWidthRatio', () => {
it('sets the ratio of the available width to the full text width', () => {
mockCanvasWidth = 10000;
setMockTextWidth(10000)(utils);
utils.setTextWidthRatio();
expect(utils.widthRatio).toEqual(0.1);
});

it('allow measuring passed text and deducting an offset width', () => {
// Note: there isn't a super great way to mock a real-world example of this
// in Jest because mockCanvasWidth applies to both the measured text and excluded text
mockCanvasWidth = 500;
setMockTextWidth(500)(utils);
utils.setTextWidthRatio('text to measure', 'some excluded text');
expect(utils.widthRatio).toEqual(1);
});
});

describe('getTextFromRatio', () => {
it('splits the passed text string by the ratio determined by `setTextWidthRatio`', () => {
mockCanvasWidth = 3000;
setMockTextWidth(3000)(utils);
utils.setTextWidthRatio(); // 0.33
// Should split the strings by the last/first third
expect(utils.getTextFromRatio('Lorem ipsum', 'start')).toEqual('psum');
Expand All @@ -99,36 +64,36 @@ describe('TruncationUtils', () => {

describe('checkIfTruncationIsNeeded', () => {
it('returns false if truncation is not needed', () => {
mockCanvasWidth = 100;
setMockTextWidth(100)(utils);
expect(utils.checkIfTruncationIsNeeded()).toEqual(false);

mockCanvasWidth = 400;
setMockTextWidth(400)(utils);
expect(utils.checkIfTruncationIsNeeded()).toBeUndefined();
});
});

describe('checkSufficientEllipsisWidth', () => {
it('returns false and errors if the container is not wide enough for the ellipsis', () => {
mockCanvasWidth = 201;
setMockTextWidth(201)(utils);
expect(utils.checkSufficientEllipsisWidth('startEnd')).toEqual(false);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'The truncation ellipsis is larger than the available width. No text can be rendered.'
);

mockCanvasWidth = 10;
setMockTextWidth(10)(utils);
expect(utils.checkSufficientEllipsisWidth('start')).toBeUndefined();
});
});

describe('checkTruncationOffsetWidth', () => {
it('returns false and errors if the container is not wide enough for the offset text', () => {
mockCanvasWidth = 201;
setMockTextWidth(201)(utils);
expect(utils.checkTruncationOffsetWidth('hello')).toEqual(false);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'The passed truncationOffset is too large for the available width. Truncating the offset instead.'
);

mockCanvasWidth = 200;
setMockTextWidth(200)(utils);
expect(utils.checkTruncationOffsetWidth('world')).toBeUndefined();
});
});
Expand Down
36 changes: 0 additions & 36 deletions src/components/text_truncate/utils.testenv.ts

This file was deleted.

60 changes: 1 addition & 59 deletions src/components/text_truncate/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,72 +6,14 @@
* Side Public License, v 1.
*/

import type { ExclusiveUnion } from '../common';
import { CanvasTextParams, CanvasTextUtils } from '../../services/canvas';

type CanvasTextParams = ExclusiveUnion<
{ container: HTMLElement },
{ font: CanvasTextDrawingStyles['font'] }
>;
type TruncationParams = CanvasTextParams & {
fullText: string;
ellipsis: string;
availableWidth: number;
};

/**
* Under the hood, a temporary Canvas element is created for manipulating text
* & determining text width.
*
* To accurately measure text, canvas rendering requires either a container to
* compute/derive font styles from, or a static font string (useful for usage
* outside the DOM). Particular care should be applied when fallback fonts are
* used, as more fallback fonts can lead to less precision.
*
* Please note that while canvas is more significantly more performant than DOM
* measurement, there are subpixel to single digit pixel differences between
* DOM and canvas measurement due to the different rendering engines used.
*/
export class CanvasTextUtils {
context: CanvasRenderingContext2D;
currentText = '';

constructor({ font, container }: CanvasTextParams) {
this.context = document.createElement('canvas').getContext('2d')!;

// Set the canvas font to ensure text width calculations are correct
if (font) {
this.context.font = font;
} else if (container) {
this.context.font = this.computeFontFromElement(container);
}
}

computeFontFromElement = (element: HTMLElement) => {
const computedStyles = window.getComputedStyle(element);
// TODO: font-stretch is not included even though it potentially should be
// @see https://developer.mozilla.org/en-US/docs/Web/CSS/font#constituent_properties
// It appears to be unsupported and/or breaks font computation in canvas
return [
'font-style',
'font-variant',
'font-weight',
'font-size',
'font-family',
]
.map((prop) => computedStyles.getPropertyValue(prop))
.join(' ')
.trim();
};

get textWidth() {
return this.context.measureText(this.currentText).width;
}

setTextToCheck = (text: string) => {
this.currentText = text;
};
}

/**
* Utilities for truncating types at various positions, as well as
* determining whether truncation is possible or even necessary.
Expand Down
49 changes: 49 additions & 0 deletions src/services/canvas/canvas_text_utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { CanvasTextUtils } from './canvas_text_utils';

let mockCanvasWidth = 0;
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
value: () => ({ measureText: () => ({ width: mockCanvasWidth }), font: '' }),
});

describe('CanvasTextUtils', () => {
describe('font calculations', () => {
it('computes the set font if passed a container element', () => {
const container = document.createElement('div');
container.style.font = '14px Inter';

const utils = new CanvasTextUtils({ container });
expect(utils.context.font).toEqual('14px Inter');
});

it('accepts a static font string', () => {
const utils = new CanvasTextUtils({ font: '14px Inter' });
expect(utils.context.font).toEqual('14px Inter');
});
});

describe('text width utils', () => {
const utils = new CanvasTextUtils({ font: '' });

describe('textWidth', () => {
it('returns the measured text width from the canvas', () => {
mockCanvasWidth = 200;
expect(utils.textWidth).toEqual(200);
});
});

describe('setTextToCheck', () => {
it('sets the internal currentText variable', () => {
utils.setTextToCheck('hello world');
expect(utils.currentText).toEqual('hello world');
});
});
});
});
20 changes: 20 additions & 0 deletions src/services/canvas/canvas_text_utils.testenv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export class CanvasTextUtils {
constructor(_: any) {}

computeFontFromElement = (_: HTMLElement) => '';

textWidth = 0;

currentText = '';
setTextToCheck = (text: string) => {
this.currentText = text;
};
}
Loading

0 comments on commit 1fa8cf0

Please sign in to comment.