mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 17:19:57 +00:00
fix(frontend): clear pinned scope from neutral interaction surfaces
This commit is contained in:
parent
fb4f81dcf2
commit
3300ece8dd
18 changed files with 310 additions and 158 deletions
|
|
@ -152,7 +152,7 @@ work extends shared components instead of creating new local variants.
|
|||
8. Keep summary chart interaction identity on one shared helper. Summary surfaces that expose row-hover, group-hover, chart-hover, or route-focus-driven chart emphasis must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` and pass that same resolved scope into card-state, sparkline, and density-map primitives, rather than letting cards read `hovered || focused` while charts listen to a different page-local ID source. Hovering one summary chart must promote that series into the shared active entity so sibling cards highlight the same object instead of keeping chart-local hover islands, and hovering or pinning a workload group header, infrastructure cluster header, or storage pool-group header must scope the matching summary cards through that same shared contract instead of forking a page-local summary filter path. Sibling cards should surface that synchronized hover as one compact header readout through the shared summary-card contract, while the chart under the pointer keeps the only floating tooltip. `frontend-modern/src/components/Recovery/RecoverySummary.tsx` is explicitly outside this interaction dialect: recovery posture cards may share summary framing, but they must not silently grow row/group/chart hover behavior without a separate governed product decision.
|
||||
9. Keep page summaries page-scoped when table rows enter contextual focus. Route-backed row selection may add a focused label and shared series emphasis, but infrastructure, workloads, and storage summary cards must continue to render the page-level series set instead of collapsing the summary down to the selected row or replacing the global trend view with row-local empty states.
|
||||
10. Keep contextual row focus on the shared summary primitive. Summary surfaces and same-route table drill-ins must reuse `frontend-modern/src/components/shared/contextualFocus.ts` for interactive-series filtering, focused-name lookup, active-series derivation, local scroll preservation, and deliberate inline-detail reveal instead of rebuilding page-local `Set` filters, focused-label scans, drawer-aware scroll math, or ad hoc scroll restoration in each surface.
|
||||
11. Keep summary-to-table coordination deliberate, explicit, and reversible. Shared summary hover may highlight the matching table row when it is already visible, but transient chart hover must not auto-filter tables, auto-scroll the page, or reshuffle table ordering. Pinned page/group/entity scope on workloads, infrastructure, or storage must stay row-first: the pinned row or group header is the visible scoped state, not a second strip or search-row widget. Page shells therefore must not reintroduce always-on scope banners, preview bars, page-local chips, breadcrumbs, or search/filter-row scope accessories just to explain pinned state. When the active row is off-screen, page owners must still route through `frontend-modern/src/components/shared/summaryTableFocus.ts` and surface a lightweight `Jump to row` affordance that reveals and scrolls only on explicit user action. That same shared table-focus owner now also owns whitespace clearing: clicking a governed `data-summary-clear-surface` table-surface root may clear pinned scope, but row cells, group headers, inline detail, and explicit controls must not accidentally clear it. Deliberate row focus may reveal inline detail automatically, but that reveal must be drawer-aware: infrastructure and workload row toggles that already have the row in view must hand the current `.app-scroll-shell` position through `frontend-modern/src/utils/appShellScrollRestoration.ts` so the remounted shell in `frontend-modern/src/App.tsx` can reopen the inline detail without looking like a page refresh, and then still route through the shared reveal helper whenever the opened drawer would otherwise land below the fold. Same-route drawers must therefore scroll only enough to keep the row header plus the top of the inline detail visible, never hard-center the row just because the route state changed.
|
||||
11. Keep summary-to-table coordination deliberate, explicit, and reversible. Shared summary hover may highlight the matching table row when it is already visible, but transient chart hover must not auto-filter tables, auto-scroll the page, or reshuffle table ordering. Pinned page/group/entity scope on workloads, infrastructure, or storage must stay row-first: the pinned row or group header is the visible scoped state, not a second strip or search-row widget. Page shells therefore must not reintroduce always-on scope banners, preview bars, page-local chips, breadcrumbs, or search/filter-row scope accessories just to explain pinned state. When the active row is off-screen, page owners must still route through `frontend-modern/src/components/shared/summaryTableFocus.ts` and surface a lightweight `Jump to row` affordance that reveals and scrolls only on explicit user action. That same shared table-focus owner now also owns whitespace clearing: pinned scope may clear only from governed neutral interaction-surface space, with page owners binding a broader clear-surface root separately from the row-lookup table root when needed. Row cells, group headers, inline detail, summary cards, and explicit controls must not accidentally clear pinned scope, while governed table/card clear surfaces must still allow real user clicks on neutral whitespace to clear it. Deliberate row focus may reveal inline detail automatically, but that reveal must be drawer-aware: infrastructure and workload row toggles that already have the row in view must hand the current `.app-scroll-shell` position through `frontend-modern/src/utils/appShellScrollRestoration.ts` so the remounted shell in `frontend-modern/src/App.tsx` can reopen the inline detail without looking like a page refresh, and then still route through the shared reveal helper whenever the opened drawer would otherwise land below the fold. Same-route drawers must therefore scroll only enough to keep the row header plus the top of the inline detail visible, never hard-center the row just because the route state changed.
|
||||
Shared summary-linked rows and group headers must also route their preview
|
||||
semantics through
|
||||
`frontend-modern/src/components/shared/summaryInteractionA11y.ts`.
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ regression protection.
|
|||
24. Extend dashboard shell rendering through `frontend-modern/src/components/Dashboard/DashboardStateCards.tsx`, `frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx`, and `frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx` rather than accreting loading cards, workload table markup, or stats-strip presentation back into `frontend-modern/src/components/Dashboard/Dashboard.tsx`
|
||||
25. Extend dashboard workload table shell ownership through `frontend-modern/src/components/Dashboard/WorkloadTableHeader.tsx` and `frontend-modern/src/components/Dashboard/WorkloadPanel.tsx` rather than rebuilding sortable header markup, grouped node rows, row expansion, or guest-drawer rendering inside `frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx`
|
||||
26. Keep long-range workload chart capping time-proportional across `frontend-modern/src/components/Workloads/WorkloadsSummary.tsx`, `frontend-modern/src/api/charts.ts`, and `internal/api/router.go`: when the workload hot path caps mixed-cadence history for top cards, it must bucket by time window rather than raw point index so 7-day and 30-day workload cards stay visually even without relaxing the protected payload budget.
|
||||
27. Keep summary hover/focus and sticky-card behavior on shared hot paths: infrastructure, workloads, and storage summary shells must reuse one page/group/entity scope model plus `frontend-modern/src/components/shared/StickySummarySection.tsx` inside the app scroll shell instead of per-page scroll listeners or per-card hover derivations, so row scrubbing highlights all cards, workload group headers, infrastructure cluster headers, and storage pool-group headers scope the summary coherently, pinned group focus remains route-backed and reversible, and the hot path does not multiply render or scroll work. That hot path stays row-first rather than adding fallback chrome: the on-screen row or group header is the scoped state, and clearing belongs to governed `data-summary-clear-surface` table-surface roots instead of bolting one-off clear buttons or search-row widgets into each page shell. That same hot path must keep chart-backed summary-card geometry explicit and stable so hover rerenders, synchronized readouts, or idle header metadata cannot feed layout loops that grow or shrink the top cards over time. Recovery’s summary rail is not part of this interactive hot path; it may share summary-card framing, but it must remain non-interactive until a separately governed model says otherwise.
|
||||
27. Keep summary hover/focus and sticky-card behavior on shared hot paths: infrastructure, workloads, and storage summary shells must reuse one page/group/entity scope model plus `frontend-modern/src/components/shared/StickySummarySection.tsx` inside the app scroll shell instead of per-page scroll listeners or per-card hover derivations, so row scrubbing highlights all cards, workload group headers, infrastructure cluster headers, and storage pool-group headers scope the summary coherently, pinned group focus remains route-backed and reversible, and the hot path does not multiply render or scroll work. That hot path stays row-first rather than adding fallback chrome: the on-screen row or group header is the scoped state, and clearing belongs to governed neutral interaction surfaces instead of bolting one-off clear buttons or search-row widgets into each page shell. The same hot path must therefore separate row-lookup roots from broader clear-surface roots when page whitespace should clear pinned scope. That same hot path must keep chart-backed summary-card geometry explicit and stable so hover rerenders, synchronized readouts, or idle header metadata cannot feed layout loops that grow or shrink the top cards over time. Recovery’s summary rail is not part of this interactive hot path; it may share summary-card framing, but it must remain non-interactive until a separately governed model says otherwise.
|
||||
The input path for that hot summary contract must stay shared too:
|
||||
`frontend-modern/src/components/shared/summaryInteractionA11y.ts` owns
|
||||
fine-pointer preview and focus-preview continuity, while
|
||||
|
|
|
|||
|
|
@ -202,9 +202,10 @@ querying, and the operator-facing storage health presentation layer.
|
|||
leaving stale row-local IDs or storage-local hover branches on the page.
|
||||
Any page, group, or entity scope that becomes pinned through storage
|
||||
interaction must stay row-first: the pinned row or group remains the
|
||||
visible scoped state, and clearing belongs to the governed storage content
|
||||
surface root via `data-summary-clear-surface` rather than an extra storage-
|
||||
local strip or search-row widget.
|
||||
visible scoped state, and clearing belongs to governed neutral
|
||||
interaction-surface whitespace rather than an extra storage-local strip or
|
||||
search-row widget, with storage owning a broader clear-surface root
|
||||
separately from the content-card row-lookup root.
|
||||
When that scope is a storage
|
||||
pool group, member pool rows should expose shared
|
||||
`data-summary-group-member-active="preview|pinned"` state so the grouped
|
||||
|
|
|
|||
|
|
@ -216,9 +216,11 @@ assembly branch.
|
|||
query state, so pinned scope is shareable, reversible, and owned by the
|
||||
same route-backed summary contract as row focus. Infrastructure must stay
|
||||
row-first here: the pinned cluster header remains the visible scoped
|
||||
state, and clearing belongs to the governed infrastructure table surface
|
||||
root via `data-summary-clear-surface` rather than a search-row fallback
|
||||
widget or a second scope/pinned pill inside the cluster row chrome.
|
||||
state, and clearing belongs to governed neutral interaction-surface
|
||||
whitespace rather than a search-row fallback widget or a second
|
||||
scope/pinned pill inside the cluster row chrome, with infrastructure
|
||||
owning a broader clear-surface root separately from the table row-lookup
|
||||
root.
|
||||
14. Keep infrastructure row emphasis on the shared frontend presentation
|
||||
contract. Host, PBS, and PMG table sections may decide whether a resource
|
||||
is contextually active, but they must expose that state through
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function Dashboard(props: DashboardProps) {
|
|||
const state = useDashboardState(props);
|
||||
|
||||
return (
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-3" data-testid="workloads-page">
|
||||
<Show when={state.isWorkloadsRoute() && !state.workloadsSummaryCollapsed()}>
|
||||
<StickySummarySection>
|
||||
<WorkloadsSummary
|
||||
|
|
@ -60,88 +60,96 @@ export function Dashboard(props: DashboardProps) {
|
|||
workloads={state.workloads}
|
||||
/>
|
||||
|
||||
<Show
|
||||
when={
|
||||
!state.kioskMode() &&
|
||||
state.surfaceConnected() &&
|
||||
state.surfaceInitialDataReceived() &&
|
||||
state.allGuests().length > 0
|
||||
}
|
||||
<div
|
||||
ref={state.setClearSurfaceRootRef}
|
||||
class="space-y-3"
|
||||
data-testid="workloads-interaction-surface"
|
||||
>
|
||||
<DashboardFilter
|
||||
search={state.search}
|
||||
setSearch={state.setSearch}
|
||||
viewMode={state.viewMode}
|
||||
setViewMode={state.setViewMode}
|
||||
statusMode={state.statusMode}
|
||||
setStatusMode={state.setStatusMode}
|
||||
groupingMode={state.groupingMode}
|
||||
setGroupingMode={state.setGroupingMode}
|
||||
setSortKey={state.setSortKey}
|
||||
setSortDirection={state.setSortDirection}
|
||||
onBeforeAutoFocus={state.handleBeforeAutoFocus}
|
||||
columnVisibility={state.dashboardFilterColumnVisibility()}
|
||||
chartsCollapsed={state.isWorkloadsRoute() ? state.workloadsSummaryCollapsed : undefined}
|
||||
onChartsToggle={
|
||||
state.isWorkloadsRoute()
|
||||
? () => state.setWorkloadsSummaryCollapsed((collapsed) => !collapsed)
|
||||
: undefined
|
||||
<Show
|
||||
when={
|
||||
!state.kioskMode() &&
|
||||
state.surfaceConnected() &&
|
||||
state.surfaceInitialDataReceived() &&
|
||||
state.allGuests().length > 0
|
||||
}
|
||||
containerRuntimeFilter={state.containerRuntimeFilterConfig()}
|
||||
hostFilter={state.hostFilterConfig()}
|
||||
namespaceFilter={state.namespaceFilterConfig()}
|
||||
platformFilter={state.platformFilterConfig()}
|
||||
/>
|
||||
</Show>
|
||||
>
|
||||
<div data-summary-clear-ignore>
|
||||
<DashboardFilter
|
||||
search={state.search}
|
||||
setSearch={state.setSearch}
|
||||
viewMode={state.viewMode}
|
||||
setViewMode={state.setViewMode}
|
||||
statusMode={state.statusMode}
|
||||
setStatusMode={state.setStatusMode}
|
||||
groupingMode={state.groupingMode}
|
||||
setGroupingMode={state.setGroupingMode}
|
||||
setSortKey={state.setSortKey}
|
||||
setSortDirection={state.setSortDirection}
|
||||
onBeforeAutoFocus={state.handleBeforeAutoFocus}
|
||||
columnVisibility={state.dashboardFilterColumnVisibility()}
|
||||
chartsCollapsed={state.isWorkloadsRoute() ? state.workloadsSummaryCollapsed : undefined}
|
||||
onChartsToggle={
|
||||
state.isWorkloadsRoute()
|
||||
? () => state.setWorkloadsSummaryCollapsed((collapsed) => !collapsed)
|
||||
: undefined
|
||||
}
|
||||
containerRuntimeFilter={state.containerRuntimeFilterConfig()}
|
||||
hostFilter={state.hostFilterConfig()}
|
||||
namespaceFilter={state.namespaceFilterConfig()}
|
||||
platformFilter={state.platformFilterConfig()}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={
|
||||
state.surfaceConnected() &&
|
||||
state.surfaceInitialDataReceived() &&
|
||||
state.filteredGuests().length > 0
|
||||
}
|
||||
>
|
||||
<DashboardWorkloadTable
|
||||
activeAlerts={state.activeAlerts}
|
||||
alertsEnabled={state.alertsEnabled}
|
||||
bottomSpacerHeight={state.bottomSpacerHeight}
|
||||
getGroupLabel={state.getGroupLabel}
|
||||
groupedGuests={state.groupedGuests}
|
||||
groupedWindowing={state.groupedWindowing}
|
||||
guestMetadata={state.guestMetadata}
|
||||
guestParentNodeMap={state.guestParentNodeMap}
|
||||
groupingMode={state.groupingMode}
|
||||
handleCustomUrlUpdate={state.handleCustomUrlUpdate}
|
||||
handleSort={state.handleSort}
|
||||
handleTagClick={state.handleTagClick}
|
||||
activeSummaryWorkloadGroupScope={state.activeSummaryWorkloadGroupScope}
|
||||
activeSummaryWorkloadId={state.activeSummaryWorkloadId}
|
||||
focusedSummaryWorkloadGroupScope={state.focusedSummaryWorkloadGroupScope}
|
||||
focusedSummaryWorkloadGroupId={state.focusedSummaryWorkloadGroupId}
|
||||
hoveredSummaryWorkloadGroupScope={state.hoveredSummaryWorkloadGroupScope}
|
||||
isMobile={state.isMobile}
|
||||
mobileVisibleColumnIds={state.mobileVisibleColumnIds}
|
||||
mobileVisibleColumns={state.mobileVisibleColumns}
|
||||
nodeByInstance={state.nodeByInstance}
|
||||
search={state.search}
|
||||
selectedGuestId={state.selectedGuestId}
|
||||
setFocusedWorkloadGroupScope={state.setFocusedWorkloadGroupScope}
|
||||
setHoveredWorkloadGroupScope={state.setHoveredWorkloadGroupScope}
|
||||
setHoveredWorkloadId={state.setHoveredWorkloadId}
|
||||
setSelectedGuestId={state.setSelectedGuestId}
|
||||
setTableRootRef={state.setTableRootRef}
|
||||
setTableBodyRef={state.setTableBodyRef}
|
||||
setTableWrapperRef={state.setTableWrapperRef}
|
||||
sortDirection={state.sortDirection}
|
||||
sortKey={state.sortKey}
|
||||
topSpacerHeight={state.topSpacerHeight}
|
||||
totalColumns={state.totalColumns}
|
||||
visibleColumns={state.visibleColumns}
|
||||
visibleGroupKeys={state.visibleGroupKeys}
|
||||
windowedGroupedGuests={state.windowedGroupedGuests}
|
||||
workloadIOEmphasis={state.workloadIOEmphasis}
|
||||
/>
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
state.surfaceConnected() &&
|
||||
state.surfaceInitialDataReceived() &&
|
||||
state.filteredGuests().length > 0
|
||||
}
|
||||
>
|
||||
<DashboardWorkloadTable
|
||||
activeAlerts={state.activeAlerts}
|
||||
alertsEnabled={state.alertsEnabled}
|
||||
bottomSpacerHeight={state.bottomSpacerHeight}
|
||||
getGroupLabel={state.getGroupLabel}
|
||||
groupedGuests={state.groupedGuests}
|
||||
groupedWindowing={state.groupedWindowing}
|
||||
guestMetadata={state.guestMetadata}
|
||||
guestParentNodeMap={state.guestParentNodeMap}
|
||||
groupingMode={state.groupingMode}
|
||||
handleCustomUrlUpdate={state.handleCustomUrlUpdate}
|
||||
handleSort={state.handleSort}
|
||||
handleTagClick={state.handleTagClick}
|
||||
activeSummaryWorkloadGroupScope={state.activeSummaryWorkloadGroupScope}
|
||||
activeSummaryWorkloadId={state.activeSummaryWorkloadId}
|
||||
focusedSummaryWorkloadGroupScope={state.focusedSummaryWorkloadGroupScope}
|
||||
focusedSummaryWorkloadGroupId={state.focusedSummaryWorkloadGroupId}
|
||||
hoveredSummaryWorkloadGroupScope={state.hoveredSummaryWorkloadGroupScope}
|
||||
isMobile={state.isMobile}
|
||||
mobileVisibleColumnIds={state.mobileVisibleColumnIds}
|
||||
mobileVisibleColumns={state.mobileVisibleColumns}
|
||||
nodeByInstance={state.nodeByInstance}
|
||||
search={state.search}
|
||||
selectedGuestId={state.selectedGuestId}
|
||||
setFocusedWorkloadGroupScope={state.setFocusedWorkloadGroupScope}
|
||||
setHoveredWorkloadGroupScope={state.setHoveredWorkloadGroupScope}
|
||||
setHoveredWorkloadId={state.setHoveredWorkloadId}
|
||||
setSelectedGuestId={state.setSelectedGuestId}
|
||||
setTableRootRef={state.setTableRootRef}
|
||||
setTableBodyRef={state.setTableBodyRef}
|
||||
setTableWrapperRef={state.setTableWrapperRef}
|
||||
sortDirection={state.sortDirection}
|
||||
sortKey={state.sortKey}
|
||||
topSpacerHeight={state.topSpacerHeight}
|
||||
totalColumns={state.totalColumns}
|
||||
visibleColumns={state.visibleColumns}
|
||||
visibleGroupKeys={state.visibleGroupKeys}
|
||||
windowedGroupedGuests={state.windowedGroupedGuests}
|
||||
workloadIOEmphasis={state.workloadIOEmphasis}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<DashboardStatsStrip
|
||||
connected={state.surfaceConnected}
|
||||
|
|
|
|||
|
|
@ -723,7 +723,10 @@ describe('Dashboard performance contract', () => {
|
|||
expect(dashboardSource).not.toContain('SummaryScopeBar');
|
||||
expect(dashboardSource).not.toContain('searchTrailing={pinnedScopeFallback()}');
|
||||
expect(dashboardSource).not.toContain('mobileTrailing={pinnedScopeFallback()}');
|
||||
expect(dashboardSource).toContain('setClearSurfaceRootRef');
|
||||
expect(dashboardSource).toContain('setTableRootRef={state.setTableRootRef}');
|
||||
expect(dashboardSource).toContain('data-testid="workloads-interaction-surface"');
|
||||
expect(dashboardSource).toContain('data-summary-clear-ignore');
|
||||
expect(dashboardWorkloadTableSource).toContain('data-summary-clear-surface');
|
||||
expect(dashboardWorkloadTableSource).toContain('data-testid="workloads-table-surface"');
|
||||
expect(dashboardFilterSource).not.toContain('const [filtersOpen, setFiltersOpen] =');
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ export function useDashboardSelectionState(options: UseDashboardSelectionStateOp
|
|||
summaryInteraction.setTableRootRef(element);
|
||||
};
|
||||
|
||||
const setClearSurfaceRootRef = (element: HTMLDivElement | undefined) => {
|
||||
summaryInteraction.setClearSurfaceRootRef(element);
|
||||
};
|
||||
|
||||
const setSelectedGuestIdState = (id: string | null) => {
|
||||
preserveScrollableAncestorVerticalOffset(tableWrapperRef(), () => {
|
||||
setSelectedGuestIdRaw(id);
|
||||
|
|
@ -228,6 +232,7 @@ export function useDashboardSelectionState(options: UseDashboardSelectionStateOp
|
|||
revealedGuestId,
|
||||
selectedGuestId,
|
||||
setChartHoverSync: summaryInteraction.setChartHoverSync,
|
||||
setClearSurfaceRootRef,
|
||||
setFocusedWorkloadGroupScope,
|
||||
setHoveredWorkloadGroupScope,
|
||||
setHoveredWorkloadId,
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ export function useDashboardState(props: DashboardProps) {
|
|||
revealedGuestId,
|
||||
selectedGuestId,
|
||||
setChartHoverSync,
|
||||
setClearSurfaceRootRef,
|
||||
setFocusedWorkloadGroupScope,
|
||||
setHoveredWorkloadGroupScope,
|
||||
setHoveredWorkloadId,
|
||||
|
|
@ -321,6 +322,7 @@ export function useDashboardState(props: DashboardProps) {
|
|||
selectedNode,
|
||||
setContainerRuntime,
|
||||
setChartHoverSync,
|
||||
setClearSurfaceRootRef,
|
||||
setFocusedWorkloadGroupScope,
|
||||
setGroupingMode,
|
||||
setHoveredWorkloadGroupScope,
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const Storage: Component = () => {
|
|||
jumpToActiveStorageRow,
|
||||
selectedDiskId,
|
||||
setChartHoverSync,
|
||||
setClearSurfaceRootRef,
|
||||
setFocusedStorageGroupScope,
|
||||
setHoveredStorageGroupScope,
|
||||
setHoveredStorageResourceId,
|
||||
|
|
@ -66,7 +67,7 @@ const Storage: Component = () => {
|
|||
} = useStoragePageModel();
|
||||
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-4" data-testid="storage-page">
|
||||
<StickySummarySection desktopOnly={false}>
|
||||
<StoragePageSummary
|
||||
filteredRecordCount={() => filteredRecords().length}
|
||||
|
|
@ -91,60 +92,68 @@ const Storage: Component = () => {
|
|||
isCephRecord={isStorageRecordCeph}
|
||||
/>
|
||||
|
||||
<StoragePageControls
|
||||
kioskMode={kioskMode}
|
||||
view={view}
|
||||
setView={setView}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
groupBy={groupBy}
|
||||
setGroupBy={setGroupBy}
|
||||
sortKey={sortKey}
|
||||
setSortKey={setSortKey}
|
||||
sortDirection={sortDirection}
|
||||
setSortDirection={setSortDirection}
|
||||
statusFilter={storageFilterStatus}
|
||||
setStatusFilter={setStorageFilterStatus}
|
||||
sourceFilter={sourceFilter}
|
||||
setSourceFilter={setSourceFilter}
|
||||
sourceOptions={sourceFilterOptions}
|
||||
nodeFilterOptions={nodeFilterOptions()}
|
||||
selectedNodeId={selectedNodeId}
|
||||
setSelectedNodeId={setSelectedNodeId}
|
||||
storageFilterGroupBy={storageFilterGroupBy}
|
||||
/>
|
||||
<div
|
||||
ref={setClearSurfaceRootRef}
|
||||
class="space-y-4"
|
||||
data-testid="storage-interaction-surface"
|
||||
>
|
||||
<div data-summary-clear-ignore>
|
||||
<StoragePageControls
|
||||
kioskMode={kioskMode}
|
||||
view={view}
|
||||
setView={setView}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
groupBy={groupBy}
|
||||
setGroupBy={setGroupBy}
|
||||
sortKey={sortKey}
|
||||
setSortKey={setSortKey}
|
||||
sortDirection={sortDirection}
|
||||
setSortDirection={setSortDirection}
|
||||
statusFilter={storageFilterStatus}
|
||||
setStatusFilter={setStorageFilterStatus}
|
||||
sourceFilter={sourceFilter}
|
||||
setSourceFilter={setSourceFilter}
|
||||
sourceOptions={sourceFilterOptions}
|
||||
nodeFilterOptions={nodeFilterOptions()}
|
||||
selectedNodeId={selectedNodeId}
|
||||
setSelectedNodeId={setSelectedNodeId}
|
||||
storageFilterGroupBy={storageFilterGroupBy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<StoragePageBanners kind={activeBannerKind} reconnect={reconnect} />
|
||||
<StoragePageBanners kind={activeBannerKind} reconnect={reconnect} />
|
||||
|
||||
<StorageContentCard
|
||||
view={view}
|
||||
physicalDisks={physicalDisks}
|
||||
nodes={nodes}
|
||||
selectedNodeId={selectedNodeId}
|
||||
search={search}
|
||||
groupedRecords={groupedRecords}
|
||||
groupBy={groupBy}
|
||||
expandedGroups={expandedGroups}
|
||||
toggleGroup={toggleGroup}
|
||||
expandedPoolId={expandedPoolId}
|
||||
setExpandedPoolId={setExpandedPoolId}
|
||||
nodeOnlineByLabel={nodeOnlineByLabel}
|
||||
highlightedRecordId={highlightedRecordId}
|
||||
getRecordAlertState={getRecordAlertState}
|
||||
isLoadingPools={isLoadingPools}
|
||||
activeSummaryGroupScope={activeSummaryStorageGroupScope}
|
||||
hoveredSummaryGroupScope={hoveredSummaryStorageGroupScope}
|
||||
focusedSummaryGroupScope={focusedSummaryStorageGroupScope}
|
||||
focusedSummaryGroupId={focusedSummaryStorageGroupId}
|
||||
onGroupFocusChange={setFocusedStorageGroupScope}
|
||||
onGroupHoverChange={setHoveredStorageGroupScope}
|
||||
highlightedSummaryResourceId={activeSummaryStorageResourceId}
|
||||
hoveredStorageResourceId={hoveredStorageResourceId}
|
||||
setTableRootRef={setSummaryTableRootRef}
|
||||
setHoveredStorageResourceId={setHoveredStorageResourceId}
|
||||
selectedDiskId={selectedDiskId}
|
||||
setSelectedDiskId={setSelectedDiskId}
|
||||
/>
|
||||
<StorageContentCard
|
||||
view={view}
|
||||
physicalDisks={physicalDisks}
|
||||
nodes={nodes}
|
||||
selectedNodeId={selectedNodeId}
|
||||
search={search}
|
||||
groupedRecords={groupedRecords}
|
||||
groupBy={groupBy}
|
||||
expandedGroups={expandedGroups}
|
||||
toggleGroup={toggleGroup}
|
||||
expandedPoolId={expandedPoolId}
|
||||
setExpandedPoolId={setExpandedPoolId}
|
||||
nodeOnlineByLabel={nodeOnlineByLabel}
|
||||
highlightedRecordId={highlightedRecordId}
|
||||
getRecordAlertState={getRecordAlertState}
|
||||
isLoadingPools={isLoadingPools}
|
||||
activeSummaryGroupScope={activeSummaryStorageGroupScope}
|
||||
hoveredSummaryGroupScope={hoveredSummaryStorageGroupScope}
|
||||
focusedSummaryGroupScope={focusedSummaryStorageGroupScope}
|
||||
focusedSummaryGroupId={focusedSummaryStorageGroupId}
|
||||
onGroupFocusChange={setFocusedStorageGroupScope}
|
||||
onGroupHoverChange={setHoveredStorageGroupScope}
|
||||
highlightedSummaryResourceId={activeSummaryStorageResourceId}
|
||||
hoveredStorageResourceId={hoveredStorageResourceId}
|
||||
setTableRootRef={setSummaryTableRootRef}
|
||||
setHoveredStorageResourceId={setHoveredStorageResourceId}
|
||||
selectedDiskId={selectedDiskId}
|
||||
setSelectedDiskId={setSelectedDiskId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -815,7 +815,7 @@ describe('Storage', () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
|
||||
const clearSurface = screen.getByTestId('storage-content-surface');
|
||||
const clearSurface = screen.getByTestId('storage-interaction-surface');
|
||||
expect(clearSurface).not.toBeNull();
|
||||
|
||||
fireEvent.click(clearSurface);
|
||||
|
|
|
|||
|
|
@ -410,6 +410,7 @@ export const useStoragePageModel = () => {
|
|||
hoveredSummaryStorageGroupScope: hoveredStorageGroupScope,
|
||||
selectedDiskId,
|
||||
setChartHoverSync: summaryInteraction.setChartHoverSync,
|
||||
setClearSurfaceRootRef: summaryInteraction.setClearSurfaceRootRef,
|
||||
setFocusedStorageGroupScope,
|
||||
setHoveredStorageGroupScope,
|
||||
setHoveredStorageResourceId,
|
||||
|
|
|
|||
|
|
@ -313,6 +313,8 @@ describe('shared primitive guardrails', () => {
|
|||
expect(summaryTableFocusSource).toContain('revealInlineDetailInViewport');
|
||||
expect(summaryTableFocusSource).toContain('MutationObserver');
|
||||
expect(summaryTableFocusSource).toContain('clearPinnedScope?: () => void;');
|
||||
expect(summaryTableFocusSource).toContain('setClearSurfaceRootRef');
|
||||
expect(summaryTableFocusSource).toContain('[data-summary-clear-ignore]');
|
||||
expect(summaryTableFocusSource).toContain("target.closest('[data-summary-clear-surface]')");
|
||||
expect(summaryTableFocusSource).toContain('querySelector<HTMLElement>(');
|
||||
expect(summaryTableFocusSource).toContain(
|
||||
|
|
|
|||
|
|
@ -156,11 +156,12 @@ describe('useSummaryPageInteractionState', () => {
|
|||
it('clears pinned scope when operators click table whitespace on a clear surface', () => {
|
||||
const [focusedSeriesId] = createSignal<string | null>('workload-a');
|
||||
const clearPinnedScope = vi.fn();
|
||||
const clearRoot = document.createElement('div');
|
||||
const root = document.createElement('div');
|
||||
root.setAttribute('data-summary-clear-surface', '');
|
||||
const filler = document.createElement('div');
|
||||
root.appendChild(filler);
|
||||
document.body.appendChild(root);
|
||||
clearRoot.append(root, filler);
|
||||
document.body.appendChild(clearRoot);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryPageInteractionState({
|
||||
|
|
@ -170,6 +171,7 @@ describe('useSummaryPageInteractionState', () => {
|
|||
);
|
||||
|
||||
result.setTableRootRef(root);
|
||||
result.setClearSurfaceRootRef(clearRoot);
|
||||
filler.click();
|
||||
|
||||
expect(clearPinnedScope).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -178,12 +180,14 @@ describe('useSummaryPageInteractionState', () => {
|
|||
it('does not clear pinned scope when operators click an active summary row', () => {
|
||||
const [focusedSeriesId] = createSignal<string | null>('workload-a');
|
||||
const clearPinnedScope = vi.fn();
|
||||
const clearRoot = document.createElement('div');
|
||||
const root = document.createElement('div');
|
||||
root.setAttribute('data-summary-clear-surface', '');
|
||||
const row = document.createElement('div');
|
||||
row.setAttribute('data-summary-series-id', 'workload-a');
|
||||
clearRoot.append(root);
|
||||
root.appendChild(row);
|
||||
document.body.appendChild(root);
|
||||
document.body.appendChild(clearRoot);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryPageInteractionState({
|
||||
|
|
@ -193,8 +197,35 @@ describe('useSummaryPageInteractionState', () => {
|
|||
);
|
||||
|
||||
result.setTableRootRef(root);
|
||||
result.setClearSurfaceRootRef(clearRoot);
|
||||
row.click();
|
||||
|
||||
expect(clearPinnedScope).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not clear pinned scope when operators click ignored controls inside the clear root', () => {
|
||||
const [focusedGroupId] = createSignal<string | null>('group-a');
|
||||
const clearPinnedScope = vi.fn();
|
||||
const clearRoot = document.createElement('div');
|
||||
const root = document.createElement('div');
|
||||
const controls = document.createElement('div');
|
||||
const input = document.createElement('input');
|
||||
controls.setAttribute('data-summary-clear-ignore', '');
|
||||
controls.appendChild(input);
|
||||
clearRoot.append(controls, root);
|
||||
document.body.appendChild(clearRoot);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSummaryPageInteractionState({
|
||||
clearPinnedScope,
|
||||
focusedGroupId,
|
||||
}),
|
||||
);
|
||||
|
||||
result.setTableRootRef(root);
|
||||
result.setClearSurfaceRootRef(clearRoot);
|
||||
input.click();
|
||||
|
||||
expect(clearPinnedScope).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const SUMMARY_CLEAR_IGNORE_SELECTOR = [
|
|||
'[role="button"]',
|
||||
'[role="link"]',
|
||||
'[role="menuitem"]',
|
||||
'[data-summary-clear-ignore]',
|
||||
'[data-summary-series-id]',
|
||||
'[data-summary-group-id]',
|
||||
'[data-inline-detail-for]',
|
||||
|
|
@ -74,6 +75,7 @@ export interface UseSummaryTableFocusBridgeOptions {
|
|||
|
||||
export function useSummaryTableFocusBridge(options: UseSummaryTableFocusBridgeOptions) {
|
||||
const [tableRoot, setTableRoot] = createSignal<HTMLElement | null>(null);
|
||||
const [clearSurfaceRoot, setClearSurfaceRoot] = createSignal<HTMLElement | null>(null);
|
||||
const [viewportVersion, setViewportVersion] = createSignal(0);
|
||||
const focusedSeriesId = options.focusedSeriesId ?? (() => null);
|
||||
const focusedGroupId = options.focusedGroupId ?? (() => null);
|
||||
|
|
@ -130,8 +132,9 @@ export function useSummaryTableFocusBridge(options: UseSummaryTableFocusBridgeOp
|
|||
});
|
||||
|
||||
createEffect(() => {
|
||||
const root = tableRoot();
|
||||
if (!root || !options.clearPinnedScope) {
|
||||
const root = clearSurfaceRoot() ?? tableRoot();
|
||||
const lookupRoot = tableRoot();
|
||||
if (!root || !lookupRoot || !options.clearPinnedScope) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -143,10 +146,10 @@ export function useSummaryTableFocusBridge(options: UseSummaryTableFocusBridgeOp
|
|||
if (!(target instanceof HTMLElement) || !root.contains(target)) {
|
||||
return;
|
||||
}
|
||||
if (!target.closest('[data-summary-clear-surface]')) {
|
||||
if (target.closest(SUMMARY_CLEAR_IGNORE_SELECTOR)) {
|
||||
return;
|
||||
}
|
||||
if (target.closest(SUMMARY_CLEAR_IGNORE_SELECTOR)) {
|
||||
if (lookupRoot.contains(target) && !target.closest('[data-summary-clear-surface]')) {
|
||||
return;
|
||||
}
|
||||
options.clearPinnedScope?.();
|
||||
|
|
@ -345,6 +348,7 @@ export function useSummaryTableFocusBridge(options: UseSummaryTableFocusBridgeOp
|
|||
activeRow,
|
||||
isActiveRowVisible,
|
||||
jumpToActiveRow,
|
||||
setClearSurfaceRootRef: (element: HTMLElement | undefined) => setClearSurfaceRoot(element ?? null),
|
||||
setTableRootRef: (element: HTMLElement | undefined) => setTableRoot(element ?? null),
|
||||
shouldShowJumpToActiveRow,
|
||||
} as const;
|
||||
|
|
@ -409,6 +413,7 @@ export function useSummaryPageInteractionState(options: UseSummaryPageInteractio
|
|||
activeSeriesId,
|
||||
chartHoverSync,
|
||||
jumpToActiveRow: tableFocus.jumpToActiveRow,
|
||||
setClearSurfaceRootRef: tableFocus.setClearSurfaceRootRef,
|
||||
setChartHoverSync,
|
||||
setTableRootRef: tableFocus.setTableRootRef,
|
||||
shouldShowJumpToActiveRow: tableFocus.shouldShowJumpToActiveRow,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export function InfrastructurePageSurface() {
|
|||
filteredResources,
|
||||
hasFilteredResources,
|
||||
setChartHoverSync,
|
||||
setSummaryClearSurfaceRootRef,
|
||||
setSummaryTableRootRef,
|
||||
shouldShowJumpToActiveResourceRow,
|
||||
} = useInfrastructurePageState();
|
||||
|
|
@ -160,7 +161,13 @@ export function InfrastructurePageSurface() {
|
|||
</StickySummarySection>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
ref={setSummaryClearSurfaceRootRef}
|
||||
class="space-y-3"
|
||||
data-testid="infrastructure-interaction-surface"
|
||||
>
|
||||
<Show when={!kioskMode()}>
|
||||
<div data-summary-clear-ignore>
|
||||
<Card padding="sm" class="mb-4">
|
||||
<PageControls
|
||||
search={
|
||||
|
|
@ -293,6 +300,7 @@ export function InfrastructurePageSurface() {
|
|||
/>
|
||||
</PageControls>
|
||||
</Card>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
|
|
@ -339,6 +347,7 @@ export function InfrastructurePageSurface() {
|
|||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,10 @@ describe('InfrastructurePageSurface guardrails', () => {
|
|||
expect(infrastructurePageSurfaceSource).toContain('hoveredSummaryGroupScope={hoveredSummaryResourceGroupScope()}');
|
||||
expect(infrastructurePageSurfaceSource).toContain('focusedSummaryGroupScope={focusedSummaryResourceGroupScope()}');
|
||||
expect(infrastructurePageSurfaceSource).toContain('onGroupHoverChange={setHoveredResourceGroupScope}');
|
||||
expect(infrastructurePageSurfaceSource).toContain('setSummaryClearSurfaceRootRef');
|
||||
expect(infrastructurePageSurfaceSource).toContain('setTableRootRef={setSummaryTableRootRef}');
|
||||
expect(infrastructurePageSurfaceSource).toContain('data-testid="infrastructure-interaction-surface"');
|
||||
expect(infrastructurePageSurfaceSource).toContain('data-summary-clear-ignore');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('SummaryScopeBar');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('searchTrailing={pinnedScopeFallback()}');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('mobileTrailing={pinnedScopeFallback()}');
|
||||
|
|
@ -63,6 +66,7 @@ describe('InfrastructurePageSurface guardrails', () => {
|
|||
expect(infrastructurePageStateSource).toContain('clearPinnedSummaryScope');
|
||||
expect(infrastructurePageStateSource).toContain('setHoveredResourceGroupScope');
|
||||
expect(infrastructurePageStateSource).toContain('jumpToActiveResourceRow');
|
||||
expect(infrastructurePageStateSource).toContain('setSummaryClearSurfaceRootRef');
|
||||
expect(infrastructurePageStateSource).toContain('setSummaryTableRootRef');
|
||||
expect(infrastructurePageStateSource).toContain('shouldShowJumpToActiveResourceRow');
|
||||
expect(infrastructurePageStateSource).toContain('preserveScrollableAncestorVerticalOffset');
|
||||
|
|
|
|||
|
|
@ -121,6 +121,10 @@ export function useInfrastructurePageState() {
|
|||
summaryInteraction.setTableRootRef(element);
|
||||
};
|
||||
|
||||
const setSummaryClearSurfaceRootRef = (element: HTMLDivElement | undefined) => {
|
||||
summaryInteraction.setClearSurfaceRootRef(element);
|
||||
};
|
||||
|
||||
const preserveTableScrollAnchor = (apply: () => void) => {
|
||||
preserveScrollableAncestorVerticalOffset(tableRootRef(), apply);
|
||||
};
|
||||
|
|
@ -245,6 +249,7 @@ export function useInfrastructurePageState() {
|
|||
setExpandedResourceId,
|
||||
setChartHoverSync: summaryInteraction.setChartHoverSync,
|
||||
setHoveredResourceGroupScope,
|
||||
setSummaryClearSurfaceRootRef,
|
||||
setSummaryTableRootRef,
|
||||
shouldShowJumpToActiveResourceRow: summaryInteraction.shouldShowJumpToActiveRow,
|
||||
setFocusedResourceGroupId,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,62 @@ async function readSummaryHighlightCount(
|
|||
);
|
||||
}
|
||||
|
||||
async function findNeutralClearPoint(
|
||||
page: import("@playwright/test").Page,
|
||||
surfaceTestId: string,
|
||||
tableTestId: string,
|
||||
): Promise<{ x: number; y: number }> {
|
||||
return page.evaluate(
|
||||
({ surfaceTestId, tableTestId }) => {
|
||||
const surface = document.querySelector<HTMLElement>(`[data-testid="${surfaceTestId}"]`);
|
||||
const table = document.querySelector<HTMLElement>(`[data-testid="${tableTestId}"]`);
|
||||
if (!surface) {
|
||||
throw new Error(`Missing surface ${surfaceTestId}`);
|
||||
}
|
||||
if (!table) {
|
||||
throw new Error(`Missing table ${tableTestId}`);
|
||||
}
|
||||
|
||||
const ignoreSelector = [
|
||||
'button',
|
||||
'a',
|
||||
'input',
|
||||
'select',
|
||||
'textarea',
|
||||
'label',
|
||||
'summary',
|
||||
'[role="button"]',
|
||||
'[role="link"]',
|
||||
'[role="menuitem"]',
|
||||
'[data-summary-clear-ignore]',
|
||||
'[data-summary-series-id]',
|
||||
'[data-summary-group-id]',
|
||||
'[data-inline-detail-for]',
|
||||
].join(', ');
|
||||
|
||||
const surfaceRect = surface.getBoundingClientRect();
|
||||
for (let y = surfaceRect.top + 12; y <= surfaceRect.bottom - 12; y += 12) {
|
||||
for (let x = surfaceRect.left + 12; x <= surfaceRect.right - 12; x += 12) {
|
||||
const target = document.elementFromPoint(x, y);
|
||||
if (!(target instanceof HTMLElement) || !surface.contains(target)) {
|
||||
continue;
|
||||
}
|
||||
if (target.closest(ignoreSelector)) {
|
||||
continue;
|
||||
}
|
||||
if (table.contains(target) && !target.closest('[data-summary-clear-surface]')) {
|
||||
continue;
|
||||
}
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`No neutral clear point found inside ${surfaceTestId}`);
|
||||
},
|
||||
{ surfaceTestId, tableTestId },
|
||||
);
|
||||
}
|
||||
|
||||
async function hoverRowUntilSummaryHighlights(
|
||||
page: import("@playwright/test").Page,
|
||||
rows: import("@playwright/test").Locator,
|
||||
|
|
@ -1000,13 +1056,15 @@ test.describe.serial("Summary hover selection", () => {
|
|||
{
|
||||
path: "/workloads",
|
||||
summaryTestId: "workloads-summary",
|
||||
clearSurfaceTestId: "workloads-table-surface",
|
||||
interactionSurfaceTestId: "workloads-interaction-surface",
|
||||
tableSurfaceTestId: "workloads-table-surface",
|
||||
tableRowSelector: "tr[data-guest-id]",
|
||||
},
|
||||
{
|
||||
path: "/infrastructure",
|
||||
summaryTestId: "infrastructure-summary",
|
||||
clearSurfaceTestId: "infrastructure-table-surface",
|
||||
interactionSurfaceTestId: "infrastructure-interaction-surface",
|
||||
tableSurfaceTestId: "infrastructure-table-surface",
|
||||
tableRowSelector: "tr[data-summary-series-id]",
|
||||
},
|
||||
] as const) {
|
||||
|
|
@ -1114,9 +1172,12 @@ test.describe.serial("Summary hover selection", () => {
|
|||
.toBe(true);
|
||||
await expect(page.getByText("Pinned to")).toHaveCount(0);
|
||||
|
||||
await page.getByTestId(surface.clearSurfaceTestId).evaluate((element) => {
|
||||
element.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
const clearPoint = await findNeutralClearPoint(
|
||||
page,
|
||||
surface.interactionSurfaceTestId,
|
||||
surface.tableSurfaceTestId,
|
||||
);
|
||||
await page.mouse.click(clearPoint.x, clearPoint.y);
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("summaryGroup"))
|
||||
.toBeNull();
|
||||
|
|
@ -1141,7 +1202,7 @@ test.describe.serial("Summary hover selection", () => {
|
|||
|
||||
await page.goto("/storage?group=node", { waitUntil: "domcontentloaded" });
|
||||
const storageSummary = page.getByTestId("storage-summary");
|
||||
const storageContentSurface = page.getByTestId("storage-content-surface");
|
||||
const storageInteractionSurface = page.getByTestId("storage-interaction-surface");
|
||||
await expect(storageSummary).toBeVisible();
|
||||
await expect(page.getByText("Pinned to")).toHaveCount(0);
|
||||
|
||||
|
|
@ -1241,9 +1302,13 @@ test.describe.serial("Summary hover selection", () => {
|
|||
.toBe(true);
|
||||
await expect(page.getByText("Pinned to")).toHaveCount(0);
|
||||
|
||||
await storageContentSurface.evaluate((element) => {
|
||||
element.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
const clearPoint = await findNeutralClearPoint(
|
||||
page,
|
||||
"storage-interaction-surface",
|
||||
"storage-content-surface",
|
||||
);
|
||||
await expect(storageInteractionSurface).toBeVisible();
|
||||
await page.mouse.click(clearPoint.x, clearPoint.y);
|
||||
await expect(page.locator('tr[data-summary-group-member-active="pinned"]')).toHaveCount(0);
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("summaryGroup"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue