diff --git a/frontend/src/static/js/components/test/webstatus-feature-page.test.ts b/frontend/src/static/js/components/test/webstatus-feature-page.test.ts index 6647b79fa..54f32fbb4 100644 --- a/frontend/src/static/js/components/test/webstatus-feature-page.test.ts +++ b/frontend/src/static/js/components/test/webstatus-feature-page.test.ts @@ -216,6 +216,82 @@ describe('webstatus-feature-page', () => { }); }); + describe('renderBrowserAvailability', () => { + let element: FeaturePage; + let hostElement: HTMLDivElement; + + beforeEach(async () => { + element = await fixture( + html``, + ); + hostElement = document.createElement('div'); + }); + + it('renders an explicit available status', async () => { + const actual = element.renderBrowserAvailability({status: 'available'}); + render(actual, hostElement); + const host = await fixture(hostElement); + + expect(host.textContent?.trim()).to.equal('Available'); + expect(host.querySelector('.availability')).to.have.class('available'); + expect(host.querySelector('img')?.getAttribute('src')).to.equal( + '/public/img/check.svg', + ); + }); + + it('renders an explicit unavailable status when no implementation exists', async () => { + const actual = element.renderBrowserAvailability(); + render(actual, hostElement); + const host = await fixture(hostElement); + + expect(host.textContent?.trim()).to.equal('Not available'); + expect(host.querySelector('.availability')).to.have.class('unavailable'); + expect(host.querySelector('img')?.getAttribute('src')).to.equal( + '/public/img/cross.svg', + ); + }); + }); + + describe('renderOneWPTCard', () => { + let element: FeaturePage; + let hostElement: HTMLDivElement; + + beforeEach(async () => { + element = await fixture( + html``, + ); + element.endDate = new Date('2025-01-01'); + hostElement = document.createElement('div'); + }); + + it('does not confuse available features without WPT scores with unavailable features', async () => { + element.feature = { + feature_id: 'id', + name: 'name', + browser_implementations: { + safari_ios: {status: 'available', date: '2024-01-01'}, + }, + }; + + const actual = element.renderOneWPTCard('safari_ios', 'safari_32x32.png'); + render(actual, hostElement); + const host = await fixture(hostElement); + + expect(host.querySelector('.availability')?.textContent?.trim()).to.equal( + 'Available', + ); + expect(host.querySelector('.score')?.textContent?.trim()).to.equal( + 'No score', + ); + expect(host.textContent).to.contain('Available since 2024-01-01'); + expect(host.textContent).to.contain('WPT score'); + }); + }); + describe('renderDeveloperSignal', () => { let element: FeaturePage; let hostElement: HTMLDivElement; diff --git a/frontend/src/static/js/components/webstatus-feature-page.ts b/frontend/src/static/js/components/webstatus-feature-page.ts index 553b35ce1..8f903a35a 100644 --- a/frontend/src/static/js/components/webstatus-feature-page.ts +++ b/frontend/src/static/js/components/webstatus-feature-page.ts @@ -227,6 +227,27 @@ export class FeaturePage extends BaseChartsPage { font-size: 150%; white-space: nowrap; } + .wptScore .availability { + display: flex; + align-items: center; + gap: var(--content-padding-quarter); + font-weight: 600; + } + .wptScore .availability.available { + color: var(--color-green-fg); + } + .wptScore .availability.unavailable { + color: var(--color-red-fg); + } + .wptScore .availability img { + width: 20px; + height: 20px; + flex-shrink: 0; + } + .wptScore .score-label { + color: var(--unimportant-text-color); + font-size: 85%; + } .wptScore.baseline .score { font-size: 150%; white-space: wrap; @@ -713,23 +734,45 @@ export class FeaturePage extends BaseChartsPage { `; } + renderBrowserAvailability( + browserImpl?: components['schemas']['BrowserImplementation'], + ): TemplateResult { + const available = browserImpl?.status === 'available'; + const status = available ? 'available' : 'unavailable'; + const label = available ? 'Available' : 'Not available'; + const icon = available ? 'check.svg' : 'cross.svg'; + + return html` +
+ + ${label} +
+ `; + } + renderOneWPTCard( browser: components['parameters']['browserPathParam'], icon: string, ): TemplateResult { + const browserImpl = this.feature?.browser_implementations?.[browser]; const scorePart = this.feature ? renderBrowserQuality( this.feature, {search: ''}, - {browser: browser, fallbackText: 'N/A'}, + { + browser: browser, + fallbackText: + browserImpl?.status === 'available' ? 'No score' : 'N/A', + }, ) : html``; - const browserImpl = this.feature?.browser_implementations?.[browser]; return html`
${BROWSER_ID_TO_LABEL[browser]}
+ ${this.renderBrowserAvailability(browserImpl)} +
WPT score
${scorePart}
${this.renderBrowserImpl(browserImpl)}