Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 52 additions & 17 deletions client/e2eTests/protoFleet/pages/miners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,31 +41,59 @@ export class MinersPage extends BasePage {
.toBeGreaterThanOrEqual(minerCount);
}

private async filterMinersByModel(minerType: string) {
await this.page.getByTestId("filter-dropdown-Model").click();
const popover = this.page.getByTestId("dropdown-filter-popover");
private async openAddFilterPopover() {
await this.page.getByTestId("filter-nested-filters-meta").click();
const popover = this.page.getByTestId("nested-dropdown-filter-popover");
await expect(popover).toBeVisible();
await expect(popover).toHaveCSS("opacity", "1");
await this.clickDropdownFilterOption(popover, [minerType]);
await popover.getByRole("button", { name: "Apply" }).click();
return popover;
}

private async openModelSubmenu(popover: Locator) {
await popover.getByTestId("nested-dropdown-filter-row-model").click();
// Desktop renders a portaled side submenu; phone/tablet collapses options into the
// parent popover with a "back" header. Either way the option rows for the chosen
// category become visible — return whichever container holds them.
const desktopSubmenu = this.page.getByTestId("nested-dropdown-filter-submenu-model");
const mobileBack = popover.getByTestId("nested-dropdown-filter-back");
await expect(desktopSubmenu.or(mobileBack)).toBeVisible();
if (await desktopSubmenu.isVisible().catch(() => false)) return desktopSubmenu;
return popover;
}

private async dismissAddFilterPopover() {
// Toggle the trigger to close — the trigger is never covered by its own popover, so
// this is more reliable than clicking page chrome that may not exist or may be
// intercepted by the portal-fixed popover.
await this.page.getByTestId("filter-nested-filters-meta").click();
const popover = this.page.getByTestId("nested-dropdown-filter-popover");
await expect(popover).toBeHidden();
}

private async filterMinersByModel(minerType: string) {
const popover = await this.openAddFilterPopover();
const submenu = await this.openModelSubmenu(popover);
await this.clickDropdownFilterOption(submenu, [minerType]);
await this.dismissAddFilterPopover();
}

async filterRigMiners() {
await this.filterMinersByModel(PROTO_RIG_MODEL);
await this.waitForAntminersToDisappear();
}

async filterAllMinersExceptRig() {
await this.page.getByTestId("filter-dropdown-Model").click();
const popover = this.page.getByTestId("dropdown-filter-popover");
await expect(popover).toBeVisible();
await expect(popover).toHaveCSS("opacity", "1");
await popover.getByText("Select all", { exact: true }).click();
await this.clickDropdownFilterOption(popover, [PROTO_RIG_MODEL]);

await popover.getByRole("button", { name: "Apply" }).click();
await expect(popover).toBeHidden();
const popover = await this.openAddFilterPopover();
const submenu = await this.openModelSubmenu(popover);
// Nested submenu has no select-all; toggle every non-rig option individually.
const optionRows = submenu.locator('[data-testid^="filter-option-"]');
const count = await optionRows.count();
const skipTestId = `filter-option-${PROTO_RIG_MODEL}`;
for (let i = 0; i < count; i++) {
const row = optionRows.nth(i);
const testId = await row.getAttribute("data-testid");
if (testId !== skipTestId) await row.click();
}
await this.dismissAddFilterPopover();
await this.waitForRigMinersToDisappear();
}

Expand Down Expand Up @@ -813,12 +841,19 @@ export class MinersPage extends BasePage {
}

async validateActiveFilter(filterLabel: string) {
const activeFilterButton = this.page.locator(`[data-testid*="active-filter-"]`, { hasText: filterLabel });
// Match the chip's editable summary button only — the outer chip wrapper also carries
// an `active-filter-*` testid, which would otherwise resolve two elements with the
// same text and trip Playwright's strict mode.
const activeFilterButton = this.page.locator('button[data-testid^="active-filter-"][data-testid$="-edit"]', {
hasText: filterLabel,
});
await expect(activeFilterButton).toBeVisible();
}

async validateActiveFilterNotVisible(filterLabel: string) {
const activeFilterButton = this.page.locator(`[data-testid*="active-filter-"]`, { hasText: filterLabel });
const activeFilterButton = this.page.locator('button[data-testid^="active-filter-"][data-testid$="-edit"]', {
hasText: filterLabel,
});
await expect(activeFilterButton).toHaveCount(0);
}

Expand Down
109 changes: 93 additions & 16 deletions client/e2eTests/protoFleet/pages/racks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,28 +310,105 @@ export class RacksPage extends BasePage {
await this.clickButton("View grid");
}

async applyZoneFilter(zoneNames: string[]) {
await this.clickVisibleFilterDropdown("Zone");
const popover = this.page.getByTestId("dropdown-filter-popover");
await expect(popover).toBeVisible();

await popover.getByRole("button", { name: "Reset", exact: true }).click();
private async getVisibleAddFilterTrigger(): Promise<Locator> {
const triggers = this.page.getByTestId("filter-nested-add-filter");
const count = await triggers.count();
for (let i = 0; i < count; i++) {
const trigger = triggers.nth(i);
if (await trigger.isVisible().catch(() => false)) return trigger;
}
throw new Error("No visible Add Filter trigger found");
}

for (const zoneName of zoneNames) {
await this.clickDropdownFilterOption(popover, zoneName);
private async openVisibleAddFilter() {
const trigger = await this.getVisibleAddFilterTrigger();
await trigger.click();
const popover = this.page.getByTestId("nested-dropdown-filter-popover");
await expect(popover).toBeVisible();
return popover;
}

private async openZoneSubmenu(popover: Locator) {
await popover.getByTestId("nested-dropdown-filter-row-zone").click();
// Desktop renders a portaled side submenu; phone/tablet collapses options into the
// parent popover with a "back" header. Either way the option rows for the chosen
// category become visible — return whichever container holds them.
const desktopSubmenu = this.page.getByTestId("nested-dropdown-filter-submenu-zone");
const mobileBack = popover.getByTestId("nested-dropdown-filter-back");
await expect(desktopSubmenu.or(mobileBack)).toBeVisible();
if (await desktopSubmenu.isVisible().catch(() => false)) return desktopSubmenu;
return popover;
}

private async dismissAddFilterPopover() {
// Toggle the trigger to close — the trigger is never covered by its own popover, so
// this is more reliable than clicking page chrome that may not exist or may be
// intercepted by the portal-fixed popover.
const trigger = await this.getVisibleAddFilterTrigger();
await trigger.click();
await expect(this.page.getByTestId("nested-dropdown-filter-popover")).toBeHidden();
}

private async setZoneSelection(target: string[]) {
// Open Add Filter, drill into Zone, and toggle each option to match the desired set.
// Reading the live submenu (which reflects current selection) avoids the race in
// editing an existing chip's popover while resetAndFetch is in flight.
const popover = await this.openVisibleAddFilter();
const submenu = await this.openZoneSubmenu(popover);
const options = submenu.locator('[data-testid^="filter-option-"]');
const count = await options.count();
const wanted = new Set(target);
for (let i = 0; i < count; i++) {
const opt = options.nth(i);
const testId = await opt.getAttribute("data-testid");
if (!testId) continue;
const optionId = testId.replace(/^filter-option-/, "");
const isChecked = await opt
.locator('input[type="checkbox"]')
.isChecked()
.catch(() => false);
if (isChecked !== wanted.has(optionId)) {
await opt.click();
}
}
await this.dismissAddFilterPopover();
}

await popover.getByRole("button", { name: "Apply", exact: true }).click();
await expect(popover).toBeHidden();
async applyZoneFilter(zoneNames: string[]) {
await this.setZoneSelection(zoneNames);
}

async toggleAllZoneFilters() {
await this.clickVisibleFilterDropdown("Zone");
const popover = this.page.getByTestId("dropdown-filter-popover");
await expect(popover).toBeVisible();
await popover.getByText("Select all", { exact: true }).click();
await popover.getByRole("button", { name: "Apply", exact: true }).click();
await expect(popover).toBeHidden();
// Toggle: if any zone is currently selected, clear; otherwise select all.
const popover = await this.openVisibleAddFilter();
const submenu = await this.openZoneSubmenu(popover);
const options = submenu.locator('[data-testid^="filter-option-"]');
const count = await options.count();
let anyChecked = false;
for (let i = 0; i < count; i++) {
if (
await options
.nth(i)
.locator('input[type="checkbox"]')
.isChecked()
.catch(() => false)
) {
anyChecked = true;
break;
}
}
for (let i = 0; i < count; i++) {
const opt = options.nth(i);
const isChecked = await opt
.locator('input[type="checkbox"]')
.isChecked()
.catch(() => false);
if (isChecked === anyChecked) {
// anyChecked => clear all (uncheck checked); !anyChecked => select all (check unchecked).
await opt.click();
}
}
await this.dismissAddFilterPopover();
}

async selectGridSort(sortLabel: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
import { encodeSortToURL, parseSortFromURL } from "@/protoFleet/features/fleetManagement/utils/sortUrlParams";
import { useUsername } from "@/protoFleet/store";

import { ChevronDown, LogoAlt, Slider } from "@/shared/assets/icons";
import { ChevronDown, LogoAlt, Plus, Slider } from "@/shared/assets/icons";
import Button, { sizes, variants } from "@/shared/components/Button";
import Header from "@/shared/components/Header";
import List from "@/shared/components/List";
Expand Down Expand Up @@ -646,6 +646,7 @@ const MinerList = ({
() => ({
type: "dropdown",
title: "Status",
pluralTitle: "statuses",
value: "status",
options: [
{ id: deviceStatusFilterStates.hashing, label: "Hashing" },
Expand All @@ -662,6 +663,7 @@ const MinerList = ({
() => ({
type: "dropdown",
title: "Issues",
pluralTitle: "issues",
value: "issues",
options: [
{ id: componentIssues.controlBoard, label: "Control board issue" },
Expand All @@ -678,6 +680,7 @@ const MinerList = ({
() => ({
type: "dropdown",
title: "Model",
pluralTitle: "models",
value: "model",
options: availableModels.map((model) => ({ id: model, label: model })),
defaultOptionIds: [],
Expand All @@ -689,6 +692,7 @@ const MinerList = ({
() => ({
type: "dropdown",
title: "Groups",
pluralTitle: "groups",
value: "group",
options: availableGroups.map((g) => ({ id: String(g.id), label: g.label })),
defaultOptionIds: [],
Expand All @@ -700,6 +704,7 @@ const MinerList = ({
() => ({
type: "dropdown",
title: "Racks",
pluralTitle: "racks",
value: "rack",
options: availableRacks.map((r) => ({ id: String(r.id), label: r.label })),
defaultOptionIds: [],
Expand All @@ -711,6 +716,7 @@ const MinerList = ({
() => ({
type: "dropdown",
title: "Firmware",
pluralTitle: "firmware versions",
value: "firmware",
options: availableFirmwareVersions.map((v) => ({ id: v, label: v })),
defaultOptionIds: [],
Expand All @@ -722,6 +728,7 @@ const MinerList = ({
() => ({
type: "dropdown",
title: "Zones",
pluralTitle: "zones",
value: "zone",
options: availableZones.map((z) => ({ id: z, label: z })),
defaultOptionIds: [],
Expand All @@ -733,15 +740,11 @@ const MinerList = ({
() => [
{
type: "nestedFilterDropdown",
title: "Filters",
title: "Add Filter",
value: "filters-meta",
prefixIcon: <Plus width="w-3" />,
children: [statusFilter, modelFilter, zonesFilter, racksFilter, groupsFilter, firmwareFilter, issuesFilter],
},
statusFilter,
issuesFilter,
modelFilter,
groupsFilter,
racksFilter,
],
[statusFilter, issuesFilter, modelFilter, groupsFilter, racksFilter, firmwareFilter, zonesFilter],
);
Expand Down
Loading
Loading