From b0e15228556b3ab7ef521c4076d1f27cbce4929b Mon Sep 17 00:00:00 2001 From: Wattachai Kanawitoon <117723407+wattachai-lseg@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:37:11 +0700 Subject: [PATCH 1/3] fix(flag): resolve empty value returned by getComputedStyle on Chromium 151 --- .../src/flag/__test__/flag.cdn-prefix.test.js | 57 +++++++++++++++++++ packages/elements/src/flag/const.ts | 4 ++ packages/elements/src/flag/index.ts | 19 +++++-- 3 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 packages/elements/src/flag/__test__/flag.cdn-prefix.test.js create mode 100644 packages/elements/src/flag/const.ts diff --git a/packages/elements/src/flag/__test__/flag.cdn-prefix.test.js b/packages/elements/src/flag/__test__/flag.cdn-prefix.test.js new file mode 100644 index 000000000..b00842cf0 --- /dev/null +++ b/packages/elements/src/flag/__test__/flag.cdn-prefix.test.js @@ -0,0 +1,57 @@ +import sinon from 'sinon'; + +// import element and theme +import '@refinitiv-ui/elements/flag'; + +import '@refinitiv-ui/halo-theme/light/ef-flag.js'; +import { expect } from '@refinitiv-ui/test-helpers'; + +import { + checkRequestedUrl, + createAndWaitForLoad, + createFakeResponse, + flagName, + gbSvg, + generateUniqueName, + isEqualSvg, + responseConfigSuccess +} from './helpers/helpers.js'; + +// Must match DefaultStyle.CDN_PREFIX in src/flag/const.ts +const DEFAULT_CDN_PREFIX = 'https://cdn.refinitiv.net/public/libs/elf/assets/elf-theme-halo/resources/flags/'; + +// CDN prefix is resolved once per page by the FlagLoader singleton, so this case needs its own test file +describe('flag/cdn-prefix', function () { + let fetch; + beforeEach(function () { + fetch = sinon.stub(window, 'fetch'); + }); + afterEach(function () { + window.fetch.restore(); // remove stub + }); + + it('Should fall back to the hardcoded CDN prefix when --cdn-prefix resolves to an empty value', async function () { + createFakeResponse(gbSvg, responseConfigSuccess); + const uniqueFlagName = generateUniqueName(flagName); // to avoid caching + // `initial` makes the custom property guaranteed-invalid, so getComputedStyle returns an empty value + const el = await createAndWaitForLoad( + `` + ); + const expectedSrc = `${DEFAULT_CDN_PREFIX}${uniqueFlagName}.svg`; + + expect(el.getComputedVariable('--cdn-prefix')).to.equal( + '', + 'CSS variable should resolve to an empty value' + ); + expect(fetch.callCount).to.equal(1, 'Should make one request'); + expect(checkRequestedUrl(fetch.args, expectedSrc)).to.equal( + true, + `Requested URL should use the fallback prefix: ${expectedSrc}` + ); + + const svg = el.shadowRoot.querySelector('svg'); + + expect(svg).to.not.equal(null, 'SVG element should exist when falling back to the default prefix'); + expect(isEqualSvg(svg.outerHTML, gbSvg)).to.equal(true, 'Should render SVG, from the mock response'); + }); +}); diff --git a/packages/elements/src/flag/const.ts b/packages/elements/src/flag/const.ts new file mode 100644 index 000000000..3b6457b08 --- /dev/null +++ b/packages/elements/src/flag/const.ts @@ -0,0 +1,4 @@ +// Default to Halo theme as Elemental and Solar theme are deprecated. +export enum DefaultStyle { + CDN_PREFIX = 'https://cdn.refinitiv.net/public/libs/elf/assets/elf-theme-halo/resources/flags/' +} diff --git a/packages/elements/src/flag/index.ts b/packages/elements/src/flag/index.ts index 5c814bbbe..abbd4e645 100644 --- a/packages/elements/src/flag/index.ts +++ b/packages/elements/src/flag/index.ts @@ -12,6 +12,7 @@ import { property } from '@refinitiv-ui/core/decorators/property.js'; import { unsafeHTML } from '@refinitiv-ui/core/directives/unsafe-html.js'; import { VERSION } from '../version.js'; +import { DefaultStyle } from './const.js'; import { FlagLoader } from './utils/FlagLoader.js'; export { preload } from './utils/FlagLoader.js'; @@ -131,11 +132,17 @@ export class Flag extends BasicElement { */ protected override firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); - /** - * We have to call this here because - * polyfilled browsers only get variables at this point. - */ - this.setPrefix(); + // Chromium only issue: starting from version 151, + // when Flag is slotted into an unregistered custom element, + // getComputedStyle() will always return empty string as a style value. + // It needs to wait for the registration of the parent custom element first. + // In practice, this happens when Flag class and its style are imported before the parent's ones as following: + // import '@refinitiv-ui/elements/flag'; + // import '@refinitiv-ui/elements/panel'; + + // import '@refinitiv-ui/elements/flag/themes/halo/dark'; + // import '@refinitiv-ui/elements/panel/themes/halo/dark'; + setTimeout(() => this.setPrefix()); } /** @@ -167,7 +174,7 @@ export class Flag extends BasicElement { */ private setPrefix(): void { if (!FlagLoader.isPrefixSet) { - const CDNPrefix = this.getComputedVariable('--cdn-prefix').replace(/^('|")|('|")$/g, ''); + const CDNPrefix = this.getComputedVariable('--cdn-prefix', DefaultStyle.CDN_PREFIX); FlagLoader.setCdnPrefix(CDNPrefix); } From 31cc20983197f83c48eb44dedf7a3b631f34104e Mon Sep 17 00:00:00 2001 From: Wattachai Kanawitoon <117723407+wattachai-lseg@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:40:10 +0700 Subject: [PATCH 2/3] fix(icon): resolve empty value returned by getComputedStyle on Chromium 151 --- .../__test__/icon.cdn-prefix-fallback.test.js | 63 +++++++++++++++++++ packages/elements/src/icon/const.ts | 5 ++ packages/elements/src/icon/index.ts | 27 +++++--- 3 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js create mode 100644 packages/elements/src/icon/const.ts diff --git a/packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js b/packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js new file mode 100644 index 000000000..177efa9e2 --- /dev/null +++ b/packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js @@ -0,0 +1,63 @@ +import sinon from 'sinon'; + +// import element and theme +import '@refinitiv-ui/elements/configuration'; +import '@refinitiv-ui/elements/icon'; + +import '@refinitiv-ui/halo-theme/light/ef-icon.js'; +import { expect } from '@refinitiv-ui/test-helpers'; + +import { + checkRequestedUrl, + createAndWaitForLoad, + createFakeResponse, + iconName, + isEqualSvg, + responseConfigSuccess, + tickSvgSprite +} from './helpers/helpers.js'; + +// Must match DefaultStyle.CDN_SPRITE_PREFIX in src/icon/const.ts +const DEFAULT_CDN_SPRITE_PREFIX = + 'https://cdn.refinitiv.net/public/libs/elf/assets/elf-theme-halo/resources/sprites/icons.svg'; + +// CDN prefixes are resolved once per page by the loader singletons, so this case needs its own test file +describe('icon/cdn-prefix-fallback', function () { + let fetch; + beforeEach(function () { + fetch = sinon.stub(window, 'fetch'); + }); + afterEach(function () { + window.fetch.restore(); // remove stub + }); + + it('Should fall back to the sprite when CDN prefixes resolve to empty values', async function () { + createFakeResponse(tickSvgSprite, responseConfigSuccess); + // `initial` makes the custom properties guaranteed-invalid, so getComputedStyle returns empty values + const el = await createAndWaitForLoad( + `` + ); + + expect(el.getComputedVariable('--cdn-prefix')).to.equal( + '', + 'CSS variable should resolve to an empty value' + ); + expect(el.getComputedVariable('--cdn-sprite-prefix')).to.equal( + '', + 'CSS variable should resolve to an empty value' + ); + expect(fetch.callCount).to.equal(1, 'Should make one request'); + expect(checkRequestedUrl(fetch.args, DEFAULT_CDN_SPRITE_PREFIX)).to.equal( + true, + `Requested URL should use the fallback sprite prefix: ${DEFAULT_CDN_SPRITE_PREFIX}` + ); + + const svg = el.shadowRoot.querySelector('svg'); + + expect(svg).to.not.equal(null, 'SVG element should exist when falling back to the sprite'); + expect(isEqualSvg(svg.outerHTML, tickSvgSprite)).to.equal( + true, + 'Should render SVG, from the mock response' + ); + }); +}); diff --git a/packages/elements/src/icon/const.ts b/packages/elements/src/icon/const.ts new file mode 100644 index 000000000..98b52cc02 --- /dev/null +++ b/packages/elements/src/icon/const.ts @@ -0,0 +1,5 @@ +// Default to Halo theme as Elemental and Solar theme are deprecated. +export enum DefaultStyle { + CDN_PREFIX = '', + CDN_SPRITE_PREFIX = 'https://cdn.refinitiv.net/public/libs/elf/assets/elf-theme-halo/resources/sprites/icons.svg' +} diff --git a/packages/elements/src/icon/index.ts b/packages/elements/src/icon/index.ts index 66f1731d5..3255e5565 100644 --- a/packages/elements/src/icon/index.ts +++ b/packages/elements/src/icon/index.ts @@ -20,6 +20,7 @@ import { Deferred, isBase64svg, isUrl } from '@refinitiv-ui/utils/loader.js'; import { efConfig } from '../configuration/index.js'; import type { Config } from '../configuration/index.js'; import { VERSION } from '../version.js'; +import { DefaultStyle } from './const.js'; import { IconLoader } from './utils/IconLoader.js'; import { SpriteLoader } from './utils/SpriteLoader.js'; @@ -94,7 +95,10 @@ export class Icon extends BasicElement { if (oldValue !== value) { this.deferIconReady(); this._icon = value; - requestAnimationFrame(() => this.updateRenderer()); + // Wait for setPrefix() resolving both sprite & icon CDN prefix value before updating the renderer + void Promise.all([SpriteLoader.getCdnPrefix(), IconLoader.getCdnPrefix()]).then(() => { + this.updateRenderer(); + }); this.requestUpdate('icon', oldValue); } } @@ -180,12 +184,17 @@ export class Icon extends BasicElement { */ protected override firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); - - /** - * We have to call this here because - * polyfilled browsers only get variables at this point. - */ - this.setPrefix(); + // Chromium only issue: starting from version 151, + // when Icon is slotted into an unregistered custom element, + // getComputedStyle() will always return empty string as a style value. + // It needs to wait for the registration of the parent custom element first. + // In practice, this happens when Icon class and its style are imported before the parent's ones as following: + // import '@refinitiv-ui/elements/icon'; + // import '@refinitiv-ui/elements/panel'; + + // import '@refinitiv-ui/elements/icon/themes/halo/dark'; + // import '@refinitiv-ui/elements/panel/themes/halo/dark'; + setTimeout(() => this.setPrefix()); } protected override async getUpdateComplete(): Promise { @@ -284,12 +293,12 @@ export class Icon extends BasicElement { private setPrefix(): void { // This prefix for individual icons allows supporting custom prefix of self-managed icons. if (IconLoader.isPrefixPending) { - const CDNPrefix = this.getComputedVariable('--cdn-prefix'); + const CDNPrefix = this.getComputedVariable('--cdn-prefix', DefaultStyle.CDN_PREFIX); IconLoader.setCdnPrefix(CDNPrefix); } if (SpriteLoader.isPrefixPending) { - const CDNSpritePrefix = this.getComputedVariable('--cdn-sprite-prefix'); + const CDNSpritePrefix = this.getComputedVariable('--cdn-sprite-prefix', DefaultStyle.CDN_SPRITE_PREFIX); SpriteLoader.setCdnPrefix(CDNSpritePrefix); } } From d1810a972b00e9ac3080818bdd3c45a05234e96f Mon Sep 17 00:00:00 2001 From: Wattachai Kanawitoon <117723407+wattachai-lseg@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:29:44 +0700 Subject: [PATCH 3/3] test(flag, icon): reset load state to support all elements testing --- .../src/flag/__test__/flag.cdn-prefix.test.js | 13 ++++++++++--- packages/elements/src/flag/index.ts | 2 ++ .../__test__/icon.cdn-prefix-fallback.test.js | 19 ++++++++++++++----- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/elements/src/flag/__test__/flag.cdn-prefix.test.js b/packages/elements/src/flag/__test__/flag.cdn-prefix.test.js index b00842cf0..5fee40f07 100644 --- a/packages/elements/src/flag/__test__/flag.cdn-prefix.test.js +++ b/packages/elements/src/flag/__test__/flag.cdn-prefix.test.js @@ -2,9 +2,10 @@ import sinon from 'sinon'; // import element and theme import '@refinitiv-ui/elements/flag'; +import { FlagLoader } from '@refinitiv-ui/elements/flag'; import '@refinitiv-ui/halo-theme/light/ef-flag.js'; -import { expect } from '@refinitiv-ui/test-helpers'; +import { expect, nextFrame } from '@refinitiv-ui/test-helpers'; import { checkRequestedUrl, @@ -23,11 +24,17 @@ const DEFAULT_CDN_PREFIX = 'https://cdn.refinitiv.net/public/libs/elf/assets/elf // CDN prefix is resolved once per page by the FlagLoader singleton, so this case needs its own test file describe('flag/cdn-prefix', function () { let fetch; - beforeEach(function () { + const resetLoaders = async () => { + FlagLoader.reset(); + await nextFrame(5); + }; + beforeEach(async function () { + await resetLoaders(); fetch = sinon.stub(window, 'fetch'); }); - afterEach(function () { + afterEach(async function () { window.fetch.restore(); // remove stub + await resetLoaders(); }); it('Should fall back to the hardcoded CDN prefix when --cdn-prefix resolves to an empty value', async function () { diff --git a/packages/elements/src/flag/index.ts b/packages/elements/src/flag/index.ts index abbd4e645..91eaaae95 100644 --- a/packages/elements/src/flag/index.ts +++ b/packages/elements/src/flag/index.ts @@ -17,6 +17,8 @@ import { FlagLoader } from './utils/FlagLoader.js'; export { preload } from './utils/FlagLoader.js'; +export { FlagLoader } from './utils/FlagLoader.js'; + const EmptyTemplate = svg``; @customElement('ef-flag') diff --git a/packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js b/packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js index 177efa9e2..17961d2b4 100644 --- a/packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js +++ b/packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js @@ -2,10 +2,10 @@ import sinon from 'sinon'; // import element and theme import '@refinitiv-ui/elements/configuration'; -import '@refinitiv-ui/elements/icon'; +import { IconLoader, SpriteLoader, iconTemplateCache } from '@refinitiv-ui/elements/icon'; import '@refinitiv-ui/halo-theme/light/ef-icon.js'; -import { expect } from '@refinitiv-ui/test-helpers'; +import { expect, nextFrame } from '@refinitiv-ui/test-helpers'; import { checkRequestedUrl, @@ -21,14 +21,23 @@ import { const DEFAULT_CDN_SPRITE_PREFIX = 'https://cdn.refinitiv.net/public/libs/elf/assets/elf-theme-halo/resources/sprites/icons.svg'; -// CDN prefixes are resolved once per page by the loader singletons, so this case needs its own test file +// CDN prefixes and the sprite are resolved once per page by the loader singletons, +// so they must be reset to make this case independent of other tests sharing the same page describe('icon/cdn-prefix-fallback', function () { let fetch; - beforeEach(function () { + const resetLoaders = async () => { + IconLoader.reset(); + SpriteLoader.reset(); + iconTemplateCache.clear(); + await nextFrame(5); + }; + beforeEach(async function () { + await resetLoaders(); fetch = sinon.stub(window, 'fetch'); }); - afterEach(function () { + afterEach(async function () { window.fetch.restore(); // remove stub + await resetLoaders(); }); it('Should fall back to the sprite when CDN prefixes resolve to empty values', async function () {