Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiTextTruncate] Fix testenv mocks #7234

Merged
merged 4 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading