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..5fee40f07 --- /dev/null +++ b/packages/elements/src/flag/__test__/flag.cdn-prefix.test.js @@ -0,0 +1,64 @@ +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, nextFrame } 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; + const resetLoaders = async () => { + FlagLoader.reset(); + await nextFrame(5); + }; + beforeEach(async function () { + await resetLoaders(); + fetch = sinon.stub(window, 'fetch'); + }); + 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 () { + 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..91eaaae95 100644 --- a/packages/elements/src/flag/index.ts +++ b/packages/elements/src/flag/index.ts @@ -12,10 +12,13 @@ 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'; +export { FlagLoader } from './utils/FlagLoader.js'; + const EmptyTemplate = svg``; @customElement('ef-flag') @@ -131,11 +134,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 +176,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); } 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..17961d2b4 --- /dev/null +++ b/packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js @@ -0,0 +1,72 @@ +import sinon from 'sinon'; + +// import element and theme +import '@refinitiv-ui/elements/configuration'; +import { IconLoader, SpriteLoader, iconTemplateCache } from '@refinitiv-ui/elements/icon'; + +import '@refinitiv-ui/halo-theme/light/ef-icon.js'; +import { expect, nextFrame } 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 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; + const resetLoaders = async () => { + IconLoader.reset(); + SpriteLoader.reset(); + iconTemplateCache.clear(); + await nextFrame(5); + }; + beforeEach(async function () { + await resetLoaders(); + fetch = sinon.stub(window, 'fetch'); + }); + 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 () { + 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); } }