packages/web-components/fast-foundation/src/tabs/tabs.pw.spec.ts
import type { Locator, Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import type { FASTTab } from "../tab/tab.js";
import { fixtureURL } from "../__test__/helpers.js";
import type { FASTTabs } from "./tabs.js";
// TODO: Need to add tests for keyboard handling, activeIndicator position, and focus management
test.describe("Tabs", () => {
let page: Page;
let element: Locator;
let root: Locator;
let tablist: Locator;
let tabs: Locator;
const template = /* html */ `
<fast-tabs>
<fast-tab>Tab one</fast-tab>
<fast-tab>Tab two</fast-tab>
<fast-tab>Tab three</fast-tab>
<fast-tab-panel>Tab panel one</fast-tab-panel>
<fast-tab-panel>Tab panel two</fast-tab-panel>
<fast-tab-panel>Tab panel three</fast-tab-panel>
</fast-tabs>
`;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
element = page.locator("fast-tabs");
root = page.locator("#storybook-root");
tablist = element.locator(".tablist");
tabs = element.locator("fast-tab");
await page.goto(fixtureURL("tabs--tabs"));
await element.waitFor({ state: "attached" });
});
test.afterAll(async () => {
await page.close();
});
test("should have an internal element with a role of `tablist`", async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
<fast-tabs></fast-tabs>
`;
});
await expect(tablist).toHaveAttribute("role", "tablist");
});
test("should set a default orientation value of `horizontal` when `orientation` is not provided", async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
<fast-tabs></fast-tabs>
`;
});
await expect(element).toHaveJSProperty("orientation", "horizontal");
});
test("should set an `id` attribute on the active tab when an `id` is provided", async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
<fast-tabs></fast-tabs>
`;
});
const tabCount = await tabs.count();
for (let i = 0; i < tabCount; i++) {
const tab = tabs.nth(i);
await tab.evaluate((node, i) => {
node.id = `custom-id-${i}`;
}, i);
await expect(tab).toHaveAttribute("id", `custom-id-${i}`);
}
});
test("should set an `id` attribute on tab items with a unique ID when an `id` is NOT provided", async () => {
await root.evaluate(
(node, { template }) => {
node.innerHTML = template;
},
{ template }
);
const tabCount = await tabs.count();
for (let i = 0; i < tabCount; i++) {
const tab = tabs.nth(i);
const id = await tab.getAttribute("id");
// The ID function may not start at 0 so we need to check that the ID is unique
expect(id).toMatch(/tab-\d+/);
const tabPanel = element.locator(`#${id}`);
await expect(tabPanel).toHaveCount(1);
}
});
/* eslint-disable-next-line max-len */
test("should set `aria-labelledby` on the tab panel and `aria-controls` on the tab which corresponds to the matching ID when IDs are NOT provided", async () => {
await root.evaluate(
(node, { template }) => {
node.innerHTML = template;
},
{ template }
);
const tabCount = await tabs.count();
for (let i = 0; i < tabCount; i++) {
const tab = tabs.nth(i);
const panelId = (await tab.getAttribute("aria-controls")) as string;
expect(panelId).toMatch(/panel-\d+/);
const tabPanel = element.locator(`#${panelId}`);
await expect(tabPanel).toHaveCount(1);
const tabId = (await tab.getAttribute("id")) as string;
expect(tabId).toMatch(/tab-\d+/);
await expect(tabPanel).toHaveAttribute("aria-labelledby", tabId);
await expect(tab).toHaveAttribute("aria-controls", panelId);
}
});
/* eslint-disable-next-line max-len */
test("should set `aria-labelledby` on the tab panel and `aria-controls` on the tab which corresponds to the matching ID when IDs are NOT provided and additional tabs and panels are added", async () => {
await root.evaluate(
(node, { template }) => {
node.innerHTML = template;
},
{ template }
);
let tabCount = await tabs.count();
for (let i = 0; i < tabCount; i++) {
const tab = tabs.nth(i);
const panelId = (await tab.getAttribute("aria-controls")) as string;
expect(panelId).toMatch(/panel-\d+/);
const tabPanel = element.locator(`#${panelId}`);
await expect(tabPanel).toHaveCount(1);
const tabId = (await tab.getAttribute("id")) as string;
expect(tabId).toMatch(/tab-\d+/);
await expect(tabPanel).toHaveAttribute("aria-labelledby", tabId);
await expect(tab).toHaveAttribute("aria-controls", panelId);
}
await element.evaluate<void, FASTTabs>(node => {
const tabs = Array.from(node.querySelectorAll("fast-tab"));
const newTab = document.createElement("fast-tab");
newTab.textContent = "New Tab";
node.insertBefore(newTab, tabs[1]);
const newPanel = document.createElement("fast-tab-panel");
newPanel.textContent = "New Panel";
node.insertBefore(newPanel, tabs[1]);
});
tabCount = await tabs.count();
for (let i = 0; i < tabCount; i++) {
const tab = tabs.nth(i);
const panelId = (await tab.getAttribute("aria-controls")) as string;
expect(panelId).toMatch(/panel-\d+/);
const tabPanel = element.locator(`#${panelId}`);
await expect(tabPanel).toHaveCount(1);
const tabId = (await tab.getAttribute("id")) as string;
expect(tabId).toMatch(/tab-\d+/);
await expect(tabPanel).toHaveAttribute("aria-labelledby", tabId);
await expect(tab).toHaveAttribute("aria-controls", panelId);
}
});
test.describe("active tab", () => {
test("should set an `aria-selected` attribute on the active tab when `activeid` is provided", async () => {
await root.evaluate(
(node, { template }) => {
node.innerHTML = template;
},
{ template }
);
const secondTab = tabs.nth(1);
const secondTabId = (await secondTab.getAttribute("id")) ?? "";
await element.evaluate((node: FASTTabs, secondTabId) => {
node.activeid = secondTabId;
}, secondTabId);
await expect(secondTab).toHaveAttribute("aria-selected", "true");
});
test("should default the first tab as the active index if `activeid` is NOT provided", async () => {
await root.evaluate(
(node, { template }) => {
node.innerHTML = template;
},
{ template }
);
await expect(tabs.nth(0)).toHaveAttribute("aria-selected", "true");
await expect(element).toHaveJSProperty("activeTabIndex", 0);
});
test("should update `aria-selected` attribute on the active tab when `activeId` is updated", async () => {
await root.evaluate(
(node, { template }) => {
node.innerHTML = template;
},
{ template }
);
await expect(tabs.nth(0)).toHaveAttribute("aria-selected", "true");
const secondTab = tabs.nth(1);
const secondTabId = (await secondTab.getAttribute("id")) ?? "";
await element.evaluate((node: FASTTabs, secondTabId) => {
node.setAttribute("activeId", secondTabId);
}, secondTabId);
await expect(secondTab).toHaveAttribute("aria-selected", "true");
});
});
test.describe("active tabpanel", () => {
/* eslint-disable-next-line max-len */
test("should set an `aria-labelledby` attribute on the tabpanel with a value of the tab id when `activeid` is provided", async () => {
await root.evaluate(
(node, { template }) => {
node.innerHTML = template;
},
{ template }
);
const secondTab = tabs.nth(1);
const secondTabId = (await secondTab.getAttribute("id")) ?? "";
const tabPanels = element.locator("fast-tab-panel");
await element.evaluate((node: FASTTabs, secondTabId) => {
node.activeid = secondTabId;
}, secondTabId);
await expect(tabPanels.nth(1)).toHaveAttribute(
"aria-labelledby",
secondTabId
);
});
test("should set an attribute of hidden if the tabpanel is not active", async () => {
await root.evaluate(
(node, { template }) => {
node.innerHTML = template;
},
{ template }
);
const tabPanels = element.locator("fast-tab-panel");
await expect(tabPanels.nth(0)).not.toHaveAttribute("hidden");
await expect(tabPanels.nth(1)).toHaveAttribute("hidden");
await expect(tabPanels.nth(2)).toHaveAttribute("hidden");
});
});
test("should not allow selecting a tab that has been disabled after it has been connected", async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
<fast-tabs>
<fast-tab id="tab-1">Tab one</fast-tab>
<fast-tab id="tab-2">Tab two</fast-tab>
<fast-tab id="tab-3">Tab three</fast-tab>
<fast-tab-panel>Tab panel one</fast-tab-panel>
<fast-tab-panel>Tab panel two</fast-tab-panel>
<fast-tab-panel>Tab panel three</fast-tab-panel>
</fast-tabs>
`;
});
await (await element.elementHandle())?.waitForElementState("stable");
const firstTab = tabs.nth(0);
const firstTabId = await firstTab.getAttribute("id");
expect(firstTabId).toBe("tab-1");
await element.evaluate((node: FASTTabs, firstTabId) => {
node.activeid = `${firstTabId}`;
}, firstTabId);
await (await element.elementHandle())?.waitForElementState("stable");
await expect(element).toHaveJSProperty("activeid", firstTabId);
const secondTab = tabs.nth(1);
await secondTab.evaluate((node: FASTTab) => {
node.disabled = true;
});
await (await element.elementHandle())?.waitForElementState("stable");
await secondTab.click({ force: true });
await expect(element).toHaveJSProperty("activeid", firstTabId);
});
test("should allow selecting tab that has been enabled after it has been connected", async () => {
test.slow();
await root.evaluate(node => {
node.innerHTML = /* html */ `
<fast-tabs>
<fast-tab>Tab one</fast-tab>
<fast-tab disabled>Tab two</fast-tab>
<fast-tab>Tab three</fast-tab>
<fast-tab-panel>Tab panel one</fast-tab-panel>
<fast-tab-panel>Tab panel two</fast-tab-panel>
<fast-tab-panel>Tab panel three</fast-tab-panel>
</fast-tabs>
`;
});
const firstTab = tabs.nth(0);
const secondTab = tabs.nth(1);
const firstTabId = (await firstTab.getAttribute("id")) ?? "";
const secondTabId = (await secondTab.getAttribute("id")) ?? "";
await element.evaluate((node: FASTTabs, firstTabId) => {
node.activeid = firstTabId;
}, firstTabId);
await expect(element).toHaveJSProperty("activeid", firstTabId);
await secondTab.click({ force: true });
await expect(element).toHaveJSProperty("activeid", firstTabId);
await secondTab.evaluate((node: FASTTab) => {
node.disabled = false;
});
await (await element.elementHandle())?.waitForElementState("stable");
await secondTab.click({ force: true });
await expect(element).toHaveJSProperty("activeid", secondTabId);
});
test("should not allow selecting hidden tab using arrow keys", async () => {
test.slow();
await root.evaluate(node => {
node.innerHTML = /* html */ `
<fast-tabs>
<fast-tab>Tab one</fast-tab>
<fast-tab hidden>Tab two</fast-tab>
<fast-tab>Tab three</fast-tab>
<fast-tab-panel>Tab panel one</fast-tab-panel>
<fast-tab-panel>Tab panel two</fast-tab-panel>
<fast-tab-panel>Tab panel three</fast-tab-panel>
</fast-tabs>
`;
});
const firstTab = tabs.nth(0);
const thirdTab = tabs.nth(2);
const firstTabId = (await firstTab.getAttribute("id")) ?? "";
const thirdTabId = (await thirdTab.getAttribute("id")) ?? "";
await element.evaluate((node: FASTTabs, firstTabId) => {
node.activeid = firstTabId;
}, firstTabId);
await firstTab.press("ArrowRight");
await expect(element).toHaveJSProperty("activeid", thirdTabId);
});
test("should not allow selecting hidden tab by pressing End", async () => {
test.slow();
await root.evaluate(node => {
node.innerHTML = /* html */ `
<fast-tabs>
<fast-tab>Tab one</fast-tab>
<fast-tab>Tab two</fast-tab>
<fast-tab hidden>Tab three</fast-tab>
<fast-tab-panel>Tab panel one</fast-tab-panel>
<fast-tab-panel>Tab panel two</fast-tab-panel>
<fast-tab-panel>Tab panel three</fast-tab-panel>
</fast-tabs>
`;
});
const firstTab = tabs.nth(0);
const secondTab = tabs.nth(1);
const firstTabId = (await firstTab.getAttribute("id")) ?? "";
const secondTabId = (await secondTab.getAttribute("id")) ?? "";
await element.evaluate((node: FASTTabs, firstTabId) => {
node.activeid = firstTabId;
}, firstTabId);
await firstTab.press("End");
await expect(element).toHaveJSProperty("activeid", secondTabId);
});
});