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)}