From 37330b9899ce44099c70bbb4744126e7b8809cd5 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 30 Sep 2026 07:28:52 -0600 Subject: [PATCH 1/6] fix: Recalculate bboxes after fonts load --- src/plot_api/plot_api.js | 22 ++++++++++++++++++++++ src/plots/plots.js | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index f48d808425b..1a93032acb7 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -198,6 +198,28 @@ function _doPlot(gd, data, layout, config) { Lib.clearResponsive(gd); } + // A plot that draws before its web font loads measures its text with the fallback font. + // Save the callback that clears the listener for teardown in `Plots.purge`. + if (!gd._clearFontListener && document.fonts) { + const onLoadingDone = (event) => { + const families = event.fontfaces.map((face) => face.family.replace(/["']/g, '').toLowerCase()); + if (!families.length) return; + + // Clear size cache to force new measurement with loaded fonts + Drawing.savedBBoxes = {}; + + for (const node of gd.querySelectorAll('[style*="font-family"]')) { + const fontFamily = node.style.fontFamily.toLowerCase(); + if (families.some((family) => fontFamily.includes(family))) { + exports._doPlot(gd); + return; + } + } + }; + document.fonts.addEventListener('loadingdone', onLoadingDone); + gd._clearFontListener = () => document.fonts.removeEventListener('loadingdone', onLoadingDone); + } + /* * start async-friendly code - now we're actually drawing things */ diff --git a/src/plots/plots.js b/src/plots/plots.js index 49726f043fb..230bdea79f0 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1470,6 +1470,10 @@ plots.purge = function(gd) { // remove responsive handler Lib.clearResponsive(gd); + // Remove web font handler + gd._clearFontListener?.(); + delete gd._clearFontListener; + // data and layout delete gd.data; delete gd.layout; From 351fe896a54bdbbb8fa2a08519b257d9ddd7ec5f Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 30 Sep 2026 07:35:54 -0600 Subject: [PATCH 2/6] Add tests --- test/jasmine/tests/plot_api_test.js | 122 ++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index d71ea4a1e77..ad18be0bedd 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -3300,3 +3300,125 @@ describe('color attribute coercion:', () => { .then(done, done.fail); }); }); + +describe('web font loading', () => { + 'use strict'; + + const fontUrl = + '/base/node_modules/@plotly/mathjax-v3/es5/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff'; + let gd; + let gd2; + let face; + let faceCount = 0; + + beforeEach(() => { + gd = createGraphDiv(); + }); + + afterEach(() => { + Plotly.purge(gd); + destroyGraphDiv(); + if (gd2) { + Plotly.purge(gd2); + gd2.remove(); + gd2 = undefined; + } + document.fonts.delete(face); + }); + + // Each test adds a new family, so that no earlier measurement can be in the bBox cache + const addFace = () => { + face = new FontFace(`PlotlyTestFont${++faceCount}`, `url(${fontUrl})`); + document.fonts.add(face); + return face.family; + }; + + const makeFigure = (family) => ({ + data: [{ y: [1, 3, 2], name: 'a long legend entry WWWWWWWW' }], + layout: { font: { family }, showlegend: true, width: 500, height: 400 } + }); + + const legendWidth = (div) => +div.querySelector('.legend .bg').getAttribute('width'); + + it('should redraw a plot with the metrics of its web font after the font loads', (done) => { + const fig = makeFigure(addFace()); + let widthBeforeLoad; + + Plotly.newPlot(gd, fig.data, fig.layout) + .then(() => { + expect(face.status).not.toBe('loaded'); + widthBeforeLoad = legendWidth(gd); + return new Promise((resolve) => gd.once('plotly_afterplot', resolve)); + }) + .then(() => { + expect(face.status).toBe('loaded'); + expect(legendWidth(gd)).not.toBe(widthBeforeLoad); + + // A plot drawn after the load must not reuse the sizes measured with the fallback font + gd2 = createGraphDiv('graph2'); + return Plotly.newPlot(gd2, fig.data, fig.layout); + }) + .then(() => { + expect(legendWidth(gd2)).toBe(legendWidth(gd)); + }) + .then(done, done.fail); + }); + + it('should not redraw a plot that does not use the loaded font', (done) => { + addFace(); + const fig = makeFigure('Arial'); + + Plotly.newPlot(gd, fig.data, fig.layout) + .then(() => { + spyOn(plotApi, '_doPlot').and.callThrough(); + return face.load(); + }) + // The `loadingdone` event can fire after the load promise resolves + .then(() => new Promise((resolve) => setTimeout(resolve, 100))) + .then(() => { + expect(plotApi._doPlot).not.toHaveBeenCalled(); + }) + .then(done, done.fail); + }); + + it('should redraw once per font load after several redraws', (done) => { + const fig = makeFigure(addFace()); + + Plotly.newPlot(gd, fig.data, fig.layout) + .then(() => Plotly.redraw(gd)) + .then(() => Plotly.redraw(gd)) + .then(() => { + expect(face.status).not.toBe('loaded'); + spyOn(plotApi, '_doPlot').and.callThrough(); + return face.loaded; + }) + // The `loadingdone` event can fire after the load promise resolves + .then(() => new Promise((resolve) => setTimeout(resolve, 100))) + .then(() => { + expect(plotApi._doPlot).toHaveBeenCalledTimes(1); + }) + .then(done, done.fail); + }); + + it('should not redraw a purged plot when its font loads', (done) => { + const fig = makeFigure(addFace()); + + Plotly.newPlot(gd, fig.data, fig.layout) + .then(() => { + expect(face.status).not.toBe('loaded'); + expect(gd._clearFontListener).toEqual(jasmine.any(Function)); + + Plotly.purge(gd); + expect(gd._clearFontListener).toBeUndefined(); + + spyOn(plotApi, '_doPlot').and.callThrough(); + return face.loaded; + }) + // The `loadingdone` event can fire after the load promise resolves + .then(() => new Promise((resolve) => setTimeout(resolve, 100))) + .then(() => { + expect(plotApi._doPlot).not.toHaveBeenCalled(); + }) + .then(done, done.fail); + }); +}); From dd658b17c9375b77e4d77f4391b47366aa624c05 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 30 Sep 2026 07:38:24 -0600 Subject: [PATCH 3/6] Update internal GraphDiv type --- src/types/core/graph-div.internal.d.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/types/core/graph-div.internal.d.ts b/src/types/core/graph-div.internal.d.ts index 4c621c8bad7..935cc17e4a2 100644 --- a/src/types/core/graph-div.internal.d.ts +++ b/src/types/core/graph-div.internal.d.ts @@ -28,6 +28,10 @@ export interface GraphContext extends Partial { * This is an HTMLDivElement with additional Plotly-specific properties. */ export interface GraphDiv extends HTMLDivElement { + /** Removes the listener that redraws the plot when a web font it uses finishes loading */ + _clearFontListener?: () => void; + /** Removes the listener that resizes the plot when `config.responsive` is on */ + _clearResponsive?: () => void; /** Resolved configuration plus internal context. */ _context?: GraphContext; /** True while an inline edit is in progress (e.g. axis title rename). */ From aa448903b388008251e81181a16d4351d0a148d2 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 30 Sep 2026 08:09:03 -0600 Subject: [PATCH 4/6] Add draftlog --- draftlogs/8094_fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/8094_fix.md diff --git a/draftlogs/8094_fix.md b/draftlogs/8094_fix.md new file mode 100644 index 00000000000..db611be4c4e --- /dev/null +++ b/draftlogs/8094_fix.md @@ -0,0 +1 @@ + - Redraw plots when a font that they use finishes loading [[#8094](https://github.com/plotly/plotly.js/pull/8094)] From b2aa6c22c8e08a0f9b62c23171b2bfa4ccf3b40f Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 30 Sep 2026 16:10:28 -0600 Subject: [PATCH 5/6] Use promises for tracking font loading because of Safari limitations --- src/lib/font_family_names.ts | 16 +++++++ src/lib/index.js | 2 + src/plot_api/plot_api.js | 58 ++++++++++++++++---------- src/plots/plots.js | 5 +-- src/types/core/graph-div.internal.d.ts | 4 +- test/jasmine/tests/lib_test.js | 39 +++++++++++++++++ test/jasmine/tests/plot_api_test.js | 32 ++++++++++---- 7 files changed, 123 insertions(+), 33 deletions(-) create mode 100644 src/lib/font_family_names.ts diff --git a/src/lib/font_family_names.ts b/src/lib/font_family_names.ts new file mode 100644 index 00000000000..3c02b675ff9 --- /dev/null +++ b/src/lib/font_family_names.ts @@ -0,0 +1,16 @@ +'use strict'; + +/** + * Split a CSS `font-family` value into lowercase family names without quotes. + * + * @param value - A `font-family` value, such as `"Open Sans", verdana, sans-serif` + * @returns The family names in order. A comma inside quotes stays part of its name. + */ +export function fontFamilyNames(value: string): string[] { + return (value.match(/"[^"]*"|'[^']*'|[^,]+/g) || []).map((name) => + name + .trim() + .replace(/^["']|["']$/g, '') + .toLowerCase() + ); +} diff --git a/src/lib/index.js b/src/lib/index.js index 22788501228..38d7dfe92d8 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -219,6 +219,8 @@ lib.cleanNumber = require('./clean_number').default; lib.slugify = require('./slugify'); +lib.fontFamilyNames = require('./font_family_names').fontFamilyNames; + lib.ensureNumber = function ensureNumber(v) { if (!isNumeric(v)) return BADNUM; v = Number(v); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 1a93032acb7..d07b08503a7 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -198,27 +198,8 @@ function _doPlot(gd, data, layout, config) { Lib.clearResponsive(gd); } - // A plot that draws before its web font loads measures its text with the fallback font. - // Save the callback that clears the listener for teardown in `Plots.purge`. - if (!gd._clearFontListener && document.fonts) { - const onLoadingDone = (event) => { - const families = event.fontfaces.map((face) => face.family.replace(/["']/g, '').toLowerCase()); - if (!families.length) return; - - // Clear size cache to force new measurement with loaded fonts - Drawing.savedBBoxes = {}; - - for (const node of gd.querySelectorAll('[style*="font-family"]')) { - const fontFamily = node.style.fontFamily.toLowerCase(); - if (families.some((family) => fontFamily.includes(family))) { - exports._doPlot(gd); - return; - } - } - }; - document.fonts.addEventListener('loadingdone', onLoadingDone); - gd._clearFontListener = () => document.fonts.removeEventListener('loadingdone', onLoadingDone); - } + // Text measured while one of these faces loads uses the fallback font + const unloadedFaces = document.fonts ? [...document.fonts].filter((face) => face.status !== 'loaded') : []; /* * start async-friendly code - now we're actually drawing things @@ -430,11 +411,46 @@ function _doPlot(gd, data, layout, config) { if (!plotDone || !plotDone.then) plotDone = Promise.resolve(); return plotDone.then(function () { + redrawAfterFontLoads(gd, unloadedFaces); emitAfterPlot(gd); return gd; }); } +/** + * Redraw a plot after the web fonts that its text uses finish loading. + * + * A plot that draws before its web font loads measures its text with the fallback font. + * The redraw happens after the draw that calls this function, once every face of a used family + * that started or finished loading during the draw settles. A face that fails to load still settles. + * A later draw or `Plots.purge` cancels a pending redraw. + * + * @param gd - The graph div that finished a draw + * @param unloadedFaces - The faces in `document.fonts` that were not loaded when the draw started + */ +async function redrawAfterFontLoads(gd, unloadedFaces) { + // The draw started these loads, or they finished during the draw + const changedFaces = unloadedFaces.filter((face) => ['loaded', 'loading'].includes(face.status)); + if (!changedFaces.length) return; + + const usedFamilies = new Set(); + for (const node of gd.querySelectorAll('[style*="font-family"]')) { + for (const family of Lib.fontFamilyNames(node.style.fontFamily)) usedFamilies.add(family); + } + const faces = changedFaces.filter((face) => usedFamilies.has(Lib.fontFamilyNames(face.family)[0])); + if (!faces.length) return; + + const token = {}; + gd._fontLoadToken = token; + await Promise.all(faces.map((face) => face.loaded.catch(() => {}))); + if (gd._fontLoadToken !== token) return; + delete gd._fontLoadToken; + + // Clear size cache to force new measurement with loaded fonts + Drawing.savedBBoxes = {}; + exports._doPlot(gd); +} + function emitAfterPlot(gd) { var fullLayout = gd._fullLayout; diff --git a/src/plots/plots.js b/src/plots/plots.js index 230bdea79f0..ad53d4dd3e9 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1470,9 +1470,8 @@ plots.purge = function(gd) { // remove responsive handler Lib.clearResponsive(gd); - // Remove web font handler - gd._clearFontListener?.(); - delete gd._clearFontListener; + // Cancel the pending redraw for web fonts + delete gd._fontLoadToken; // data and layout delete gd.data; diff --git a/src/types/core/graph-div.internal.d.ts b/src/types/core/graph-div.internal.d.ts index 935cc17e4a2..a4458741c65 100644 --- a/src/types/core/graph-div.internal.d.ts +++ b/src/types/core/graph-div.internal.d.ts @@ -28,14 +28,14 @@ export interface GraphContext extends Partial { * This is an HTMLDivElement with additional Plotly-specific properties. */ export interface GraphDiv extends HTMLDivElement { - /** Removes the listener that redraws the plot when a web font it uses finishes loading */ - _clearFontListener?: () => void; /** Removes the listener that resizes the plot when `config.responsive` is on */ _clearResponsive?: () => void; /** Resolved configuration plus internal context. */ _context?: GraphContext; /** True while an inline edit is in progress (e.g. axis title rename). */ _editing?: boolean; + /** Identifies the pending redraw for web fonts. A later draw or a purge replaces or deletes the token. */ + _fontLoadToken?: object; /** Trace data after defaults are applied. */ _fullData?: FullData[]; /** Layout after defaults are applied. */ diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 6f59958da97..39d5ac09395 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1996,6 +1996,45 @@ describe('Test lib.js:', function () { }); }); + describe('fontFamilyNames', () => { + it('should split a font stack into lowercase names', () => { + expect(Lib.fontFamilyNames('"Open Sans", verdana, Arial, sans-serif')).toEqual([ + 'open sans', + 'verdana', + 'arial', + 'sans-serif' + ]); + }); + + it('should return a single unquoted name', () => { + expect(Lib.fontFamilyNames('ReproMono')).toEqual(['repromono']); + }); + + it('should strip double and single quotes', () => { + expect(Lib.fontFamilyNames(`"Open Sans", 'Fira Code'`)).toEqual(['open sans', 'fira code']); + }); + + it('should keep a comma inside quotes as part of the name', () => { + expect(Lib.fontFamilyNames('"Foo, Bar", serif')).toEqual(['foo, bar', 'serif']); + expect(Lib.fontFamilyNames(`'Foo, Bar', serif`)).toEqual(['foo, bar', 'serif']); + }); + + it('should keep a longer name whole instead of matching a shorter one', () => { + expect(Lib.fontFamilyNames('"Open Sans Condensed", sans-serif')).toEqual([ + 'open sans condensed', + 'sans-serif' + ]); + }); + + it('should ignore spaces around names', () => { + expect(Lib.fontFamilyNames(' "Open Sans" , verdana ')).toEqual(['open sans', 'verdana']); + }); + + it('should return an empty array for an empty value', () => { + expect(Lib.fontFamilyNames('')).toEqual([]); + }); + }); + describe('isPlotDiv', function () { it('should work on plain objects', function () { expect(Lib.isPlotDiv({})).toBe(false); diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index ad18be0bedd..ddeae4a99c6 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -3327,9 +3327,10 @@ describe('web font loading', () => { }); // Each test adds a new family, so that no earlier measurement can be in the bBox cache - const addFace = () => { - face = new FontFace(`PlotlyTestFont${++faceCount}`, `url(${fontUrl})`); + const addFace = (name = `PlotlyTestFont${++faceCount}`) => { + face = new FontFace(name, `url(${fontUrl})`); document.fonts.add(face); + // The family comes back as CSS, with quotes when the name needs them return face.family; }; @@ -3364,6 +3365,23 @@ describe('web font loading', () => { .then(done, done.fail); }); + it('should redraw a plot whose quoted font family contains a comma', (done) => { + const fig = makeFigure(addFace(`PlotlyTest, Font${++faceCount}`)); + let widthBeforeLoad; + + Plotly.newPlot(gd, fig.data, fig.layout) + .then(() => { + expect(face.status).not.toBe('loaded'); + widthBeforeLoad = legendWidth(gd); + return new Promise((resolve) => gd.once('plotly_afterplot', resolve)); + }) + .then(() => { + expect(face.status).toBe('loaded'); + expect(legendWidth(gd)).not.toBe(widthBeforeLoad); + }) + .then(done, done.fail); + }); + it('should not redraw a plot that does not use the loaded font', (done) => { addFace(); const fig = makeFigure('Arial'); @@ -3373,7 +3391,7 @@ describe('web font loading', () => { spyOn(plotApi, '_doPlot').and.callThrough(); return face.load(); }) - // The `loadingdone` event can fire after the load promise resolves + // `document.fonts.ready` can resolve after the load promise .then(() => new Promise((resolve) => setTimeout(resolve, 100))) .then(() => { expect(plotApi._doPlot).not.toHaveBeenCalled(); @@ -3392,7 +3410,7 @@ describe('web font loading', () => { spyOn(plotApi, '_doPlot').and.callThrough(); return face.loaded; }) - // The `loadingdone` event can fire after the load promise resolves + // `document.fonts.ready` can resolve after the load promise .then(() => new Promise((resolve) => setTimeout(resolve, 100))) .then(() => { expect(plotApi._doPlot).toHaveBeenCalledTimes(1); @@ -3406,15 +3424,15 @@ describe('web font loading', () => { Plotly.newPlot(gd, fig.data, fig.layout) .then(() => { expect(face.status).not.toBe('loaded'); - expect(gd._clearFontListener).toEqual(jasmine.any(Function)); + expect(gd._fontLoadToken).toBeDefined(); Plotly.purge(gd); - expect(gd._clearFontListener).toBeUndefined(); + expect(gd._fontLoadToken).toBeUndefined(); spyOn(plotApi, '_doPlot').and.callThrough(); return face.loaded; }) - // The `loadingdone` event can fire after the load promise resolves + // `document.fonts.ready` can resolve after the load promise .then(() => new Promise((resolve) => setTimeout(resolve, 100))) .then(() => { expect(plotApi._doPlot).not.toHaveBeenCalled(); From c281d991f0e9922fc95363db4c030e8146ae0d0f Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 30 Sep 2026 17:11:39 -0600 Subject: [PATCH 6/6] Be more general in font check --- src/lib/font_family_names.ts | 16 ------------ src/lib/index.js | 2 -- src/plot_api/plot_api.js | 16 ++++-------- test/jasmine/tests/lib_test.js | 39 ----------------------------- test/jasmine/tests/plot_api_test.js | 27 ++++---------------- 5 files changed, 10 insertions(+), 90 deletions(-) delete mode 100644 src/lib/font_family_names.ts diff --git a/src/lib/font_family_names.ts b/src/lib/font_family_names.ts deleted file mode 100644 index 3c02b675ff9..00000000000 --- a/src/lib/font_family_names.ts +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -/** - * Split a CSS `font-family` value into lowercase family names without quotes. - * - * @param value - A `font-family` value, such as `"Open Sans", verdana, sans-serif` - * @returns The family names in order. A comma inside quotes stays part of its name. - */ -export function fontFamilyNames(value: string): string[] { - return (value.match(/"[^"]*"|'[^']*'|[^,]+/g) || []).map((name) => - name - .trim() - .replace(/^["']|["']$/g, '') - .toLowerCase() - ); -} diff --git a/src/lib/index.js b/src/lib/index.js index 38d7dfe92d8..22788501228 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -219,8 +219,6 @@ lib.cleanNumber = require('./clean_number').default; lib.slugify = require('./slugify'); -lib.fontFamilyNames = require('./font_family_names').fontFamilyNames; - lib.ensureNumber = function ensureNumber(v) { if (!isNumeric(v)) return BADNUM; v = Number(v); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index d07b08503a7..4a00e816cf7 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -418,11 +418,12 @@ function _doPlot(gd, data, layout, config) { } /** - * Redraw a plot after the web fonts that its text uses finish loading. + * Redraw a plot after the web fonts that load during its draw finish loading. * * A plot that draws before its web font loads measures its text with the fallback font. - * The redraw happens after the draw that calls this function, once every face of a used family - * that started or finished loading during the draw settles. A face that fails to load still settles. + * The redraw happens after the draw that calls this function, once every face that started or + * finished loading during the draw settles. A face that fails to load still settles. + * A font that other text loads during the draw also causes one redraw. * A later draw or `Plots.purge` cancels a pending redraw. * * @param gd - The graph div that finished a draw @@ -430,14 +431,7 @@ function _doPlot(gd, data, layout, config) { */ async function redrawAfterFontLoads(gd, unloadedFaces) { // The draw started these loads, or they finished during the draw - const changedFaces = unloadedFaces.filter((face) => ['loaded', 'loading'].includes(face.status)); - if (!changedFaces.length) return; - - const usedFamilies = new Set(); - for (const node of gd.querySelectorAll('[style*="font-family"]')) { - for (const family of Lib.fontFamilyNames(node.style.fontFamily)) usedFamilies.add(family); - } - const faces = changedFaces.filter((face) => usedFamilies.has(Lib.fontFamilyNames(face.family)[0])); + const faces = unloadedFaces.filter((face) => ['loaded', 'loading'].includes(face.status)); if (!faces.length) return; const token = {}; diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 39d5ac09395..6f59958da97 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1996,45 +1996,6 @@ describe('Test lib.js:', function () { }); }); - describe('fontFamilyNames', () => { - it('should split a font stack into lowercase names', () => { - expect(Lib.fontFamilyNames('"Open Sans", verdana, Arial, sans-serif')).toEqual([ - 'open sans', - 'verdana', - 'arial', - 'sans-serif' - ]); - }); - - it('should return a single unquoted name', () => { - expect(Lib.fontFamilyNames('ReproMono')).toEqual(['repromono']); - }); - - it('should strip double and single quotes', () => { - expect(Lib.fontFamilyNames(`"Open Sans", 'Fira Code'`)).toEqual(['open sans', 'fira code']); - }); - - it('should keep a comma inside quotes as part of the name', () => { - expect(Lib.fontFamilyNames('"Foo, Bar", serif')).toEqual(['foo, bar', 'serif']); - expect(Lib.fontFamilyNames(`'Foo, Bar', serif`)).toEqual(['foo, bar', 'serif']); - }); - - it('should keep a longer name whole instead of matching a shorter one', () => { - expect(Lib.fontFamilyNames('"Open Sans Condensed", sans-serif')).toEqual([ - 'open sans condensed', - 'sans-serif' - ]); - }); - - it('should ignore spaces around names', () => { - expect(Lib.fontFamilyNames(' "Open Sans" , verdana ')).toEqual(['open sans', 'verdana']); - }); - - it('should return an empty array for an empty value', () => { - expect(Lib.fontFamilyNames('')).toEqual([]); - }); - }); - describe('isPlotDiv', function () { it('should work on plain objects', function () { expect(Lib.isPlotDiv({})).toBe(false); diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index ddeae4a99c6..40c75212cda 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -3327,11 +3327,11 @@ describe('web font loading', () => { }); // Each test adds a new family, so that no earlier measurement can be in the bBox cache - const addFace = (name = `PlotlyTestFont${++faceCount}`) => { - face = new FontFace(name, `url(${fontUrl})`); + const addFace = () => { + const family = `PlotlyTestFont${++faceCount}`; + face = new FontFace(family, `url(${fontUrl})`); document.fonts.add(face); - // The family comes back as CSS, with quotes when the name needs them - return face.family; + return family; }; const makeFigure = (family) => ({ @@ -3365,24 +3365,7 @@ describe('web font loading', () => { .then(done, done.fail); }); - it('should redraw a plot whose quoted font family contains a comma', (done) => { - const fig = makeFigure(addFace(`PlotlyTest, Font${++faceCount}`)); - let widthBeforeLoad; - - Plotly.newPlot(gd, fig.data, fig.layout) - .then(() => { - expect(face.status).not.toBe('loaded'); - widthBeforeLoad = legendWidth(gd); - return new Promise((resolve) => gd.once('plotly_afterplot', resolve)); - }) - .then(() => { - expect(face.status).toBe('loaded'); - expect(legendWidth(gd)).not.toBe(widthBeforeLoad); - }) - .then(done, done.fail); - }); - - it('should not redraw a plot that does not use the loaded font', (done) => { + it('should not redraw a plot when a font loads after its draw', (done) => { addFace(); const fig = makeFigure('Arial');