diff --git a/components/baseline-indicator/sandbox.js b/components/baseline-indicator/sandbox.js
index d95aa229d..8df21100d 100644
--- a/components/baseline-indicator/sandbox.js
+++ b/components/baseline-indicator/sandbox.js
@@ -17,20 +17,11 @@ const SUPPORT_FULL = {
};
/** @type {import("@rari").Support} */
-const SUPPORT_NO_WEBKIT = {
- chrome: "111",
- chrome_android: "111",
- edge: "111",
+const SUPPORT_ONLY_FIREFOX = {
firefox: "113",
firefox_android: "113",
};
-/** @type {import("@rari").Support} */
-const SUPPORT_WEBKIT_ONLY = {
- safari: "15.4",
- safari_ios: "15.4",
-};
-
/** @type {import("@rari").Alternative[]} */
const ALTERNATIVES = [
{
@@ -69,7 +60,7 @@ function feature(overrides) {
function notAvailable(overrides) {
return {
baseline: false,
- support: SUPPORT_NO_WEBKIT,
+ support: SUPPORT_ONLY_FIREFOX,
feature: feature(),
...overrides,
};
@@ -99,6 +90,7 @@ function discouragedFeature(overrides) {
/**
* @type {{
+ * id: string;
* name: string;
* status: import("@rari").BaselineStatus;
* baseline?: import("@rari").Baseline;
@@ -106,36 +98,43 @@ function discouragedFeature(overrides) {
*/
const CASES = [
{
+ id: "high",
name: "high",
status: "high",
baseline: available("high"),
},
{
+ id: "high-asterisk",
name: "high, asterisk",
status: "high",
baseline: available("high", { asterisk: true }),
},
{
+ id: "low",
name: "low",
status: "low",
baseline: available("low"),
},
{
+ id: "low-asterisk",
name: "low, asterisk",
status: "low",
baseline: available("low", { asterisk: true }),
},
{
+ id: "limited",
name: "limited",
status: "limited",
baseline: notAvailable(),
},
{
+ id: "limited-asterisk",
name: "limited, asterisk",
status: "limited",
- baseline: notAvailable({ support: SUPPORT_WEBKIT_ONLY, asterisk: true }),
+ baseline: notAvailable({ asterisk: true }),
},
{
+ id: "limited-developer-signals",
name: "limited, developer signals",
status: "limited",
baseline: notAvailable({
@@ -145,11 +144,22 @@ const CASES = [
}),
},
{
+ id: "discouraged",
name: "discouraged",
status: "discouraged",
baseline: notAvailable({ feature: discouragedFeature() }),
},
{
+ id: "discouraged-asterisk",
+ name: "discouraged, asterisk ignored",
+ status: "discouraged",
+ baseline: notAvailable({
+ asterisk: true,
+ feature: discouragedFeature(),
+ }),
+ },
+ {
+ id: "discouraged-one-alternative",
name: "discouraged, one alternative",
status: "discouraged",
baseline: notAvailable({
@@ -158,6 +168,7 @@ const CASES = [
}),
},
{
+ id: "discouraged-several-alternatives",
name: "discouraged, several alternatives",
status: "discouraged",
baseline: notAvailable({
@@ -166,15 +177,18 @@ const CASES = [
}),
},
{
+ id: "discouraged-no-compatibility-data",
name: "discouraged, no compatibility data",
status: "discouraged",
},
{
+ id: "removing",
name: "removing",
status: "removing",
baseline: notAvailable({ feature: discouragedFeature() }),
},
{
+ id: "removing-one-alternative",
name: "removing, one alternative",
status: "removing",
baseline: notAvailable({
@@ -183,6 +197,7 @@ const CASES = [
}),
},
{
+ id: "removing-several-alternatives",
name: "removing, several alternatives",
status: "removing",
baseline: notAvailable({
@@ -212,8 +227,10 @@ export class BaselineIndicatorSandbox extends SandboxComponent {
baseline: testCase.baseline,
},
});
- return html`
${testCase.name}
- ${BaselineIndicator.render(docContext)}`;
+ return html`
+ ${testCase.name}
+ ${BaselineIndicator.render(docContext)}
+ `;
})}
`;
}
diff --git a/test/pageobjects/baseline-indicator.page.js b/test/pageobjects/baseline-indicator.page.js
new file mode 100644
index 000000000..f2cf8dcb9
--- /dev/null
+++ b/test/pageobjects/baseline-indicator.page.js
@@ -0,0 +1,32 @@
+import { $, browser } from "@wdio/globals";
+
+import BaselineIndicator from "./components/baseline-indicator.js";
+import SandboxPage from "./sandbox.page.js";
+
+class BaselineIndicatorPage {
+ open(locale = "en-US") {
+ return SandboxPage.open("baseline-indicator", locale);
+ }
+
+ /** @param {string} id */
+ fixture(id) {
+ return $(`#${id}`);
+ }
+
+ /** @param {string} id */
+ indicator(id) {
+ return new BaselineIndicator(() =>
+ this.fixture(id).$(".baseline-indicator"),
+ );
+ }
+
+ async clearStoredState() {
+ await browser.execute(() => localStorage.removeItem("baseline-indicator"));
+ }
+
+ async getStoredState() {
+ return browser.execute(() => localStorage.getItem("baseline-indicator"));
+ }
+}
+
+export default new BaselineIndicatorPage();
diff --git a/test/pageobjects/components/baseline-indicator.js b/test/pageobjects/components/baseline-indicator.js
new file mode 100644
index 000000000..e8b7ec456
--- /dev/null
+++ b/test/pageobjects/components/baseline-indicator.js
@@ -0,0 +1,100 @@
+import { browser } from "@wdio/globals";
+
+import Component from "./component.js";
+
+export default class BaselineIndicator extends Component {
+ get summary() {
+ return this.element.$("summary");
+ }
+
+ get statusIcon() {
+ return this.summary.$(".indicator");
+ }
+
+ get title() {
+ return this.summary.$(".status-title");
+ }
+
+ get pill() {
+ return this.summary.$(".pill");
+ }
+
+ get asterisk() {
+ return this.summary.$(".asterisk");
+ }
+
+ get browsers() {
+ return this.summary.$(".browsers");
+ }
+
+ /** @param {"chrome" | "edge" | "firefox" | "safari"} name */
+ browser(name) {
+ return this.browsers.$(`.${name}`);
+ }
+
+ /** @param {number} index */
+ engine(index) {
+ return this.browsers.$(`.engine:nth-child(${index + 1})`);
+ }
+
+ get extra() {
+ return this.element.$(".extra");
+ }
+
+ get asteriskNote() {
+ return this.extra.$(".asterisk-note");
+ }
+
+ get reason() {
+ return this.extra.$('p span[lang="en-US"]');
+ }
+
+ get reasonCode() {
+ return this.reason.$("code");
+ }
+
+ get signalsLink() {
+ return this.extra.$('[data-glean-id="baseline_link_signals"]');
+ }
+
+ /** @param {string} name */
+ alternativeLink(name) {
+ return this.extra.$(
+ `[data-glean-id="baseline_link_alternatives: ${name}"]`,
+ );
+ }
+
+ get alternativeLinks() {
+ return this.extra.$$('[data-glean-id^="baseline_link_alternatives:"]');
+ }
+
+ get compatibilityLink() {
+ return this.extra.$('[data-glean-id="baseline_link_bcd_table"]');
+ }
+
+ get learnMoreLink() {
+ return this.extra.$('[data-glean-id="baseline_link_learn_more"]');
+ }
+
+ async isOpen() {
+ return Boolean(await this.element.getProperty("open"));
+ }
+
+ /** @param {boolean} open */
+ async setOpen(open) {
+ if ((await this.isOpen()) !== open) {
+ await this.summary.click();
+ }
+ await browser.waitUntil(async () => (await this.isOpen()) === open, {
+ timeoutMsg: `Baseline indicator did not ${open ? "open" : "close"}`,
+ });
+ }
+
+ async open() {
+ await this.setOpen(true);
+ }
+
+ async close() {
+ await this.setOpen(false);
+ }
+}
diff --git a/test/pageobjects/components/component.js b/test/pageobjects/components/component.js
new file mode 100644
index 000000000..d82d6e903
--- /dev/null
+++ b/test/pageobjects/components/component.js
@@ -0,0 +1,12 @@
+/** @import { ChainablePromiseElement } from "webdriverio" */
+
+export default class Component {
+ /** @param {() => ChainablePromiseElement} elementFactory */
+ constructor(elementFactory) {
+ this.elementFactory = elementFactory;
+ }
+
+ get element() {
+ return this.elementFactory();
+ }
+}
diff --git a/test/pageobjects/sandbox.page.js b/test/pageobjects/sandbox.page.js
new file mode 100644
index 000000000..2731cd896
--- /dev/null
+++ b/test/pageobjects/sandbox.page.js
@@ -0,0 +1,16 @@
+import Page from "./page.js";
+
+class SandboxPage extends Page {
+ /**
+ * Opens a component's sandbox page.
+ * @param {string} component component directory name
+ * @param {string} [locale] page locale
+ */
+ open(component, locale = "en-US") {
+ return super.open(
+ `${encodeURIComponent(locale)}/sandbox/${encodeURIComponent(component)}`,
+ );
+ }
+}
+
+export default new SandboxPage();
diff --git a/test/specs/baseline-indicator.e2e.js b/test/specs/baseline-indicator.e2e.js
new file mode 100644
index 000000000..d4882532c
--- /dev/null
+++ b/test/specs/baseline-indicator.e2e.js
@@ -0,0 +1,309 @@
+import { browser, expect } from "@wdio/globals";
+
+import BaselineIndicatorPage from "../pageobjects/baseline-indicator.page.js";
+
+describe("Baseline indicator", () => {
+ beforeEach(async () => {
+ await BaselineIndicatorPage.open();
+ await BaselineIndicatorPage.clearStoredState();
+ await browser.refresh();
+ });
+
+ it("renders the semantic summary for every status", async () => {
+ const cases = [
+ {
+ id: "high",
+ title: "Baseline",
+ pill: "WIDELY AVAILABLE",
+ label: "Baseline Check",
+ open: false,
+ },
+ {
+ id: "low",
+ title: "Baseline 2023",
+ pill: "NEWLY AVAILABLE",
+ label: "Baseline Check",
+ open: false,
+ },
+ {
+ id: "limited",
+ title: "Limited availability",
+ label: "Baseline Cross",
+ open: false,
+ },
+ {
+ id: "discouraged",
+ title: "Deprecated",
+ label: "Baseline Discouraged",
+ open: true,
+ },
+ {
+ id: "removing",
+ title: "Deprecated",
+ pill: "TO BE REMOVED",
+ label: "Baseline Discouraged Cross",
+ open: true,
+ },
+ ];
+
+ for (const testCase of cases) {
+ const indicator = BaselineIndicatorPage.indicator(testCase.id);
+ await expect(indicator.element).toBeExisting();
+ await expect(indicator.title).toHaveText(testCase.title);
+ await expect(indicator.statusIcon).toHaveAttribute(
+ "aria-label",
+ testCase.label,
+ );
+ await expect(await indicator.isOpen()).toEqual(testCase.open);
+ await expect(await indicator.pill.isExisting()).toEqual(
+ Boolean(testCase.pill),
+ );
+ if (testCase.pill) {
+ await expect(indicator.pill).toHaveText(testCase.pill);
+ }
+ }
+ });
+
+ it("renders the appropriate explanation for each availability status", async () => {
+ const cases = [
+ {
+ id: "high",
+ text: "available across browsers since May 2023",
+ },
+ {
+ id: "low",
+ text: "Since May 2023, this feature works across the latest devices",
+ },
+ {
+ id: "limited",
+ text: "This feature is not Baseline because it does not work",
+ },
+ ];
+
+ for (const testCase of cases) {
+ const indicator = BaselineIndicatorPage.indicator(testCase.id);
+ await indicator.open();
+ await expect(indicator.extra).toHaveText(
+ expect.stringContaining(testCase.text),
+ );
+ }
+ });
+
+ it("exposes browser support through accessible names and engine titles", async () => {
+ const high = BaselineIndicatorPage.indicator("high");
+ const browserNames = /** @type {const} */ ([
+ "chrome",
+ "edge",
+ "firefox",
+ "safari",
+ ]);
+ for (const browserName of browserNames) {
+ const label = `${browserName.charAt(0).toUpperCase()}${browserName.slice(1)} check`;
+ await expect(high.browser(browserName)).toHaveAttribute(
+ "aria-label",
+ label,
+ );
+ }
+
+ const limited = BaselineIndicatorPage.indicator("limited");
+ await expect(limited.browser("chrome")).toHaveAttribute(
+ "aria-label",
+ "Chrome cross",
+ );
+ await expect(limited.browser("edge")).toHaveAttribute(
+ "aria-label",
+ "Edge cross",
+ );
+ await expect(limited.browser("firefox")).toHaveAttribute(
+ "aria-label",
+ "Firefox check",
+ );
+ await expect(limited.browser("safari")).toHaveAttribute(
+ "aria-label",
+ "Safari cross",
+ );
+ await expect(limited.engine(0)).toHaveAttribute(
+ "title",
+ "Not widely supported in Chrome and Edge",
+ );
+ await expect(limited.engine(1)).toHaveAttribute(
+ "title",
+ "Supported in Firefox",
+ );
+ await expect(limited.engine(2)).toHaveAttribute(
+ "title",
+ "Not widely supported in Safari",
+ );
+ });
+
+ it("renders the asterisk annotation only when applicable", async () => {
+ for (const id of ["high-asterisk", "low-asterisk", "limited-asterisk"]) {
+ const indicator = BaselineIndicatorPage.indicator(id);
+ await indicator.open();
+ await expect(indicator.asterisk).toHaveText("*");
+ await expect(indicator.asteriskNote).toHaveText(
+ expect.stringContaining(
+ "Some parts of this feature may have varying levels of support.",
+ ),
+ );
+ }
+
+ for (const id of ["high", "low", "limited", "discouraged-asterisk"]) {
+ const indicator = BaselineIndicatorPage.indicator(id);
+ await expect(indicator.asterisk).not.toBeExisting();
+ await expect(indicator.asteriskNote).not.toBeExisting();
+ }
+ });
+
+ it("renders the developer signals link metadata", async () => {
+ const indicator = BaselineIndicatorPage.indicator(
+ "limited-developer-signals",
+ );
+ await expect(await indicator.isOpen()).toEqual(true);
+ await expect(indicator.element).toHaveAttribute("data-open-by-default");
+ await expect(indicator.signalsLink).toHaveAttribute(
+ "href",
+ "https://example.com/signals",
+ );
+ await expect(indicator.signalsLink).toHaveAttribute("target", "_blank");
+ await expect(indicator.signalsLink).toHaveAttribute("rel", "noopener");
+ await expect(indicator.signalsLink).toHaveElementClass("external");
+ await expect(indicator.signalsLink).toHaveAttribute(
+ "data-glean-id",
+ "baseline_link_signals",
+ );
+ });
+
+ it("renders discouraged guidance and upstream reason markup", async () => {
+ const indicator = BaselineIndicatorPage.indicator("discouraged");
+ await expect(indicator.extra).toHaveText(
+ expect.stringContaining("Avoid using this feature in new projects."),
+ );
+ await expect(indicator.extra).toHaveText(
+ expect.stringContaining("This feature may be a candidate for removal"),
+ );
+ await expect(indicator.reason).toHaveAttribute("lang", "en-US");
+ await expect(indicator.reasonCode).toHaveText("");
+ });
+
+ it("renders removing guidance without candidate-removal copy", async () => {
+ const indicator = BaselineIndicatorPage.indicator("removing");
+ await expect(indicator.extra).toHaveText(
+ expect.stringContaining("This feature is pending removal from browsers."),
+ );
+ const text = await indicator.extra.getText();
+ await expect(text).not.toContain(
+ "This feature may be a candidate for removal",
+ );
+ });
+
+ it("renders one or several alternatives with their metadata", async () => {
+ const one = BaselineIndicatorPage.indicator("discouraged-one-alternative");
+ await expect(one.alternativeLinks).toBeElementsArrayOfSize(1);
+ await expect(one.extra).toHaveText(
+ expect.stringContaining("Consider using the following features instead"),
+ );
+ await expect(one.alternativeLink("flexbox")).toHaveText("flexbox");
+ await expect(one.alternativeLink("flexbox")).toHaveAttribute(
+ "href",
+ expect.stringContaining("/en-US/docs/Web/CSS/CSS_flexible_box_layout"),
+ );
+ await expect(one.alternativeLink("flexbox")).toHaveAttribute(
+ "title",
+ "CSS flexible box layout",
+ );
+
+ const several = BaselineIndicatorPage.indicator(
+ "discouraged-several-alternatives",
+ );
+ await expect(several.alternativeLinks).toBeElementsArrayOfSize(2);
+ await expect(several.extra).toHaveText(
+ expect.stringContaining("flexbox or grid"),
+ );
+ await expect(several.alternativeLink("grid")).toHaveAttribute(
+ "data-glean-id",
+ "baseline_link_alternatives: grid",
+ );
+
+ const removing = BaselineIndicatorPage.indicator(
+ "removing-several-alternatives",
+ );
+ await expect(removing.extra).toHaveText(
+ expect.stringContaining("Use the following features instead"),
+ );
+ });
+
+ it("localizes generated links and the alternatives list", async () => {
+ await BaselineIndicatorPage.open("fr");
+ const indicator = BaselineIndicatorPage.indicator(
+ "discouraged-several-alternatives",
+ );
+
+ await expect(indicator.extra).toHaveText(
+ expect.stringContaining("flexbox ou grid"),
+ );
+ await expect(indicator.alternativeLink("flexbox")).toHaveAttribute(
+ "href",
+ expect.stringContaining("/fr/docs/Web/CSS/CSS_flexible_box_layout"),
+ );
+ await expect(indicator.compatibilityLink).toHaveAttribute(
+ "href",
+ expect.stringContaining("#compatibilité_des_navigateurs"),
+ );
+ await expect(indicator.learnMoreLink).toHaveAttribute(
+ "href",
+ expect.stringContaining("/fr/docs/Glossary/Baseline/Compatibility"),
+ );
+ });
+
+ it("renders the common navigation links with telemetry metadata", async () => {
+ const indicator = BaselineIndicatorPage.indicator("high");
+ await expect(indicator.compatibilityLink).toHaveAttribute(
+ "href",
+ expect.stringContaining("#browser_compatibility"),
+ );
+ await expect(indicator.compatibilityLink).toHaveAttribute(
+ "data-glean-id",
+ "baseline_link_bcd_table",
+ );
+ await expect(indicator.learnMoreLink).toHaveAttribute(
+ "href",
+ expect.stringContaining("/en-US/docs/Glossary/Baseline/Compatibility"),
+ );
+ await expect(indicator.learnMoreLink).toHaveAttribute("target", "_blank");
+ await expect(indicator.learnMoreLink).toHaveAttribute(
+ "data-glean-id",
+ "baseline_link_learn_more",
+ );
+ });
+
+ it("handles mocked discouraged banner", async () => {
+ const discouraged = BaselineIndicatorPage.indicator(
+ "discouraged-no-compatibility-data",
+ );
+ await expect(discouraged.element).toBeExisting();
+ await expect(discouraged.title).toHaveText("Deprecated");
+ await expect(discouraged.browsers).not.toBeExisting();
+ });
+
+ it("persists user-controlled open state across reloads", async () => {
+ const indicator = BaselineIndicatorPage.indicator("high");
+ await expect(await indicator.isOpen()).toEqual(false);
+
+ await indicator.open();
+ await browser.waitUntil(
+ async () => (await BaselineIndicatorPage.getStoredState()) === "open",
+ { timeoutMsg: "Opening the indicator did not persist its state" },
+ );
+ await browser.refresh();
+ await expect(await indicator.isOpen()).toEqual(true);
+
+ await indicator.close();
+ await browser.waitUntil(
+ async () => (await BaselineIndicatorPage.getStoredState()) === null,
+ { timeoutMsg: "Closing the indicator did not clear its stored state" },
+ );
+ await browser.refresh();
+ await expect(await indicator.isOpen()).toEqual(false);
+ });
+});