feat(desktop): prioritize active conversation search

Co-authored-by: Fizz <f2701065ae39f701786d464e4c06b5fbc2fa3dda9f31f9439b3d077d8fd7fb0d@buzz.block.builderlab.xyz>
Signed-off-by: Fizz <f2701065ae39f701786d464e4c06b5fbc2fa3dda9f31f9439b3d077d8fd7fb0d@buzz.block.builderlab.xyz>
Co-authored-by: Arjun Mahanti <arjun@squareup.com>
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
Fizz
2026-08-24 12:39:40 -04:00
committed by Arjun Mahanti
parent 30ccc8bf6e
commit a523471ca5
7 changed files with 98 additions and 337 deletions

View File

@@ -92,56 +92,3 @@ export function SearchDialogInputRow({
</div>
);
}
type CurrentChannelSearchActionProps = {
channelLabel: string;
channelType: Channel["channelType"];
isSelected: boolean;
menuIndex: number;
onActivate: () => void;
onMouseEnter: () => void;
};
export function CurrentChannelSearchAction({
channelLabel,
channelType,
isSelected,
menuIndex,
onActivate,
onMouseEnter,
}: CurrentChannelSearchActionProps) {
const isDirectMessage = channelType === "dm";
return (
<div className="px-3 py-3.5">
<button
aria-selected={isSelected}
className={`flex w-full items-center gap-4 rounded-lg border border-border/75 px-3 py-3 text-left transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring ${
isSelected ? "bg-muted/55" : "bg-muted/30 hover:bg-muted/55"
}`}
data-search-result-index={menuIndex}
data-testid="search-current-channel-control"
onClick={onActivate}
onMouseEnter={onMouseEnter}
role="option"
type="button"
>
<span className="flex min-w-0 items-center gap-2">
<Search className="h-3.5 w-3.5 shrink-0" />
<span
className="min-w-0 truncate text-sm text-muted-foreground"
data-testid="search-current-scope-label"
>
{isDirectMessage ? "Search conversation with " : "Search in "}
<span className="font-medium text-foreground">{channelLabel}</span>
</span>
</span>
<span className="ml-auto shrink-0 text-xs text-muted-foreground/70">
{isDirectMessage
? "Search messages in this conversation."
: "Search messages in this channel"}
</span>
</button>
</div>
);
}

View File

@@ -11,7 +11,6 @@ import {
type SearchResult,
} from "@/features/search/ui/SearchResultItem";
import {
CurrentChannelSearchAction,
getChannelScopeLabel,
SearchDialogInputRow,
} from "@/features/search/ui/SearchScopeControls";
@@ -19,7 +18,6 @@ import { SearchResultTrailing } from "@/features/search/ui/SearchResultTrailing"
import { useSearchMenuKeyboardNavigation } from "@/features/search/ui/useSearchMenuKeyboardNavigation";
import {
getChannelActivityTime,
getSearchActionMenuIndex,
getSuggestedSearchResults,
} from "@/features/search/ui/topbarSearchSuggestions";
import type { Channel, SearchHit, UserSearchResult } from "@/shared/api/types";
@@ -60,6 +58,7 @@ const EMPTY_UNREAD_CHANNEL_COUNTS: ReadonlyMap<string, number> = new Map(),
const SEARCH_SECTION_TITLE_CLASS =
"px-2.5 pb-1.5 pt-2 text-xs font-medium text-muted-foreground/70";
const SEARCH_RESULT_SECTION_ORDER = [
"current-channel-messages",
"channels",
"direct-messages",
"people",
@@ -217,7 +216,14 @@ function getSearchHitContextLabel(
};
}
function getResultSectionKey(result: SearchResult): SearchResultSectionKey {
function getResultSectionKey(
result: SearchResult,
currentChannelId?: string | null,
): SearchResultSectionKey {
if (result.kind === "message" && result.hit.channelId === currentChannelId) {
return "current-channel-messages";
}
if (result.kind === "channel") {
return result.channel.channelType === "dm" ? "direct-messages" : "channels";
}
@@ -235,6 +241,8 @@ function getResultSectionKey(result: SearchResult): SearchResultSectionKey {
function getSectionTitle(sectionKey: SearchResultSectionKey) {
switch (sectionKey) {
case "current-channel-messages":
return "In this conversation";
case "channels":
return "Channels";
case "direct-messages":
@@ -274,11 +282,14 @@ function SearchHitContextLine({ label }: { label: SearchHitContextLabel }) {
);
}
function groupSearchResults(results: SearchResult[]): SearchResultSection[] {
function groupSearchResults(
results: SearchResult[],
currentChannelId?: string | null,
): SearchResultSection[] {
const resultsBySection = new Map<SearchResultSectionKey, SearchResult[]>();
for (const result of results) {
const sectionKey = getResultSectionKey(result);
const sectionKey = getResultSectionKey(result, currentChannelId);
const sectionResults = resultsBySection.get(sectionKey) ?? [];
sectionResults.push(result);
resultsBySection.set(sectionKey, sectionResults);
@@ -390,6 +401,7 @@ export function TopbarSearch({
debouncedQuery,
fuzzyUserCandidatesQuery,
isWaitingOnFromResolution,
prioritizedChannelSearchQuery,
query,
resultProfiles,
results,
@@ -401,13 +413,11 @@ export function TopbarSearch({
channels,
enabled: isOpen,
limit: SEARCH_RESULT_LIMIT,
prioritizedChannelId: scopeChannelId ? null : currentChannelId,
scopeChannelId,
});
const trimmedQuery = query.trim();
const isIconVariant = variant === "icon";
const currentChannel = currentChannelId
? (channelLookup.get(currentChannelId) ?? null)
: null;
const scopeChannel = scopeChannelId
? (channelLookup.get(scopeChannelId) ?? null)
: null;
@@ -416,7 +426,6 @@ export function TopbarSearch({
: null;
const currentPubkeyNormalized =
currentPubkey && normalizePubkey(currentPubkey);
const hasScopeAction = Boolean(currentChannel && !scopeChannel);
const { suggestedResults, unreadResults } = React.useMemo(
() =>
getSuggestedSearchResults(
@@ -477,8 +486,12 @@ export function TopbarSearch({
[currentPubkeyNormalized, results],
);
const searchResultSections = React.useMemo(
() => groupSearchResults(searchableResults),
[searchableResults],
() =>
groupSearchResults(
searchableResults,
scopeChannel ? null : currentChannelId,
),
[currentChannelId, scopeChannel, searchableResults],
);
const groupedSearchResults = React.useMemo(
() => searchResultSections.flatMap((section) => section.results),
@@ -489,14 +502,10 @@ export function TopbarSearch({
? []
: suggestionResults
: groupedSearchResults;
const actionMenuIndex = getSearchActionMenuIndex({
hasScopeAction,
isShowingSuggestions,
unreadResultCount: unreadResults.length,
});
const isSearchLoading =
isWaitingOnFromResolution ||
searchQuery.isLoading ||
prioritizedChannelSearchQuery.isLoading ||
fuzzyUserCandidatesQuery.isLoading ||
userSearchQuery.isLoading;
@@ -599,13 +608,6 @@ export function TopbarSearch({
window.requestAnimationFrame(() => dialogInputRef.current?.focus());
}, []);
const activateCurrentChannelScope = React.useCallback(() => {
if (!currentChannel) return;
setScopeChannelId(currentChannel.id);
setSelectedMenuIndex(0);
focusDialogInput();
}, [currentChannel, focusDialogInput]);
const removeChannelScope = React.useCallback(() => {
setScopeChannelId(null);
setSelectedMenuIndex(0);
@@ -627,9 +629,7 @@ export function TopbarSearch({
}, [isOpen]);
const handleDialogInputKeyDown = useSearchMenuKeyboardNavigation({
actionMenuIndex,
activeResults,
onActivateAction: activateCurrentChannelScope,
onOpenResult: openResult,
onRemoveScope: removeChannelScope,
query,
@@ -638,9 +638,7 @@ export function TopbarSearch({
setSelectedMenuIndex,
});
const renderSearchResultRow = (result: SearchResult, index: number) => {
const menuIndex =
actionMenuIndex !== null && index >= actionMenuIndex ? index + 1 : index;
const renderSearchResultRow = (result: SearchResult, menuIndex: number) => {
const channelDisplayName =
result.kind === "channel"
? getChannelDisplayName(result.channel, channelLabels)
@@ -795,31 +793,10 @@ export function TopbarSearch({
</div>
));
};
const currentChannelSearchAction =
currentChannel && !scopeChannel ? (
<CurrentChannelSearchAction
channelLabel={getChannelScopeLabel(
currentChannel,
channelLabels,
currentPubkey,
)}
channelType={currentChannel.channelType}
isSelected={selectedMenuIndex === actionMenuIndex}
menuIndex={actionMenuIndex ?? 0}
onActivate={activateCurrentChannelScope}
onMouseEnter={() => setSelectedMenuIndex(actionMenuIndex ?? 0)}
/>
) : null;
const searchResultContent = isShowingSuggestions ? (
scopeChannel ? null : suggestionResults.length === 0 ? (
<div className="max-h-96 overflow-y-auto">
{currentChannelSearchAction}
<div
className={cn(
"px-4 text-sm text-muted-foreground",
currentChannelSearchAction ? "pb-5" : "py-5",
)}
>
<div className="px-4 py-5 text-sm text-muted-foreground">
<p>No recent activity yet.</p>
</div>
</div>
@@ -843,7 +820,6 @@ export function TopbarSearch({
)}
</div>
) : null}
{currentChannelSearchAction}
{suggestedResults.length > 0 ? (
<div data-search-section="recent-activity">
<div className={SEARCH_SECTION_TITLE_CLASS}>
@@ -870,30 +846,17 @@ export function TopbarSearch({
)
) : isSearchLoading && searchableResults.length === 0 ? (
<div className="max-h-[min(60vh,32rem)] overflow-y-auto">
{currentChannelSearchAction}
<SearchResultsSkeleton />
</div>
) : searchQuery.error instanceof Error && searchableResults.length === 0 ? (
<div className="max-h-[min(60vh,32rem)] overflow-y-auto">
{currentChannelSearchAction}
<p
className={cn(
"px-4 text-sm text-destructive",
currentChannelSearchAction ? "pb-5" : "py-5",
)}
>
<p className="px-4 py-5 text-sm text-destructive">
{searchQuery.error.message}
</p>
</div>
) : searchableResults.length === 0 ? (
<div className="max-h-[min(60vh,32rem)] overflow-y-auto">
{currentChannelSearchAction}
<p
className={cn(
"px-4 text-sm text-muted-foreground",
currentChannelSearchAction ? "pb-5" : "py-5",
)}
>
<p className="px-4 py-5 text-sm text-muted-foreground">
No {scopeChannel ? "messages" : "matches"} for{" "}
<span className="font-semibold">{trimmedQuery}</span>
{scopeLabel ? (
@@ -911,7 +874,6 @@ export function TopbarSearch({
data-testid="search-results-list"
role="listbox"
>
{currentChannelSearchAction}
<div className="p-1.5">
{renderSearchResultSections(searchResultSections)}
</div>

View File

@@ -1,10 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
getSearchActionMenuIndex,
getSuggestedChannels,
} from "./topbarSearchSuggestions.ts";
import { getSuggestedChannels } from "./topbarSearchSuggestions.ts";
function channel(id, options = {}) {
return {
@@ -72,30 +69,3 @@ test("archived and unjoined non-DM conversations are omitted", () => {
["dm"],
);
});
test("the scope action follows unread rows while suggestions are visible", () => {
assert.equal(
getSearchActionMenuIndex({
hasScopeAction: true,
isShowingSuggestions: true,
unreadResultCount: 2,
}),
2,
);
assert.equal(
getSearchActionMenuIndex({
hasScopeAction: true,
isShowingSuggestions: true,
unreadResultCount: 0,
}),
0,
);
assert.equal(
getSearchActionMenuIndex({
hasScopeAction: false,
isShowingSuggestions: true,
unreadResultCount: 2,
}),
null,
);
});

View File

@@ -64,16 +64,3 @@ export function getSuggestedSearchResults(
unreadResults: unreadChannels.map(toResult),
};
}
export function getSearchActionMenuIndex({
hasScopeAction,
isShowingSuggestions,
unreadResultCount,
}: {
hasScopeAction: boolean;
isShowingSuggestions: boolean;
unreadResultCount: number;
}) {
if (!hasScopeAction) return null;
return isShowingSuggestions && unreadResultCount > 0 ? unreadResultCount : 0;
}

View File

@@ -3,9 +3,7 @@ import * as React from "react";
import type { SearchResult } from "@/features/search/ui/SearchResultItem";
export function useSearchMenuKeyboardNavigation({
actionMenuIndex,
activeResults,
onActivateAction,
onOpenResult,
onRemoveScope,
query,
@@ -13,9 +11,7 @@ export function useSearchMenuKeyboardNavigation({
selectedMenuIndex,
setSelectedMenuIndex,
}: {
actionMenuIndex: number | null;
activeResults: SearchResult[];
onActivateAction: () => void;
onOpenResult: (result: SearchResult) => void;
onRemoveScope: () => void;
query: string;
@@ -23,8 +19,7 @@ export function useSearchMenuKeyboardNavigation({
selectedMenuIndex: number;
setSelectedMenuIndex: React.Dispatch<React.SetStateAction<number>>;
}) {
const selectableCount =
activeResults.length + (actionMenuIndex === null ? 0 : 1);
const selectableCount = activeResults.length;
React.useEffect(() => {
setSelectedMenuIndex((current) => {
@@ -64,22 +59,12 @@ export function useSearchMenuKeyboardNavigation({
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
event.preventDefault();
if (actionMenuIndex === selectedMenuIndex) {
onActivateAction();
return;
}
const resultIndex =
actionMenuIndex !== null && selectedMenuIndex > actionMenuIndex
? selectedMenuIndex - 1
: selectedMenuIndex;
const result = activeResults[resultIndex];
const result = activeResults[selectedMenuIndex];
if (result) onOpenResult(result);
}
},
[
actionMenuIndex,
activeResults,
onActivateAction,
onOpenResult,
onRemoveScope,
query.length,

View File

@@ -104,12 +104,14 @@ export function useSearchResults({
channels: memberChannels,
enabled,
limit = 12,
prioritizedChannelId,
scopeChannelId,
}: {
channelLabels?: Record<string, string>;
channels: Channel[];
enabled: boolean;
limit?: number;
prioritizedChannelId?: string | null;
scopeChannelId?: string | null;
}) {
const [query, setQuery] = React.useState("");
@@ -255,13 +257,40 @@ export function useSearchResults({
unresolvedOperator: hasUnresolvedOperator,
minimumQueryLength,
});
const prioritizedChannelSearchQuery = useSearchMessagesQuery(ftsQuery, {
enabled:
enabled &&
!scopeChannelId &&
Boolean(prioritizedChannelId) &&
channelResolution.status === "none" &&
!hasUnresolvedOperator &&
!waitingOnFromResolution &&
ftsQuery.length >= minimumQueryLength,
limit,
channelId: prioritizedChannelId ?? undefined,
authors:
authorResolution.status === "resolved"
? [authorResolution.value]
: undefined,
since: parsedQuery.since,
until: parsedQuery.until,
unresolvedOperator: hasUnresolvedOperator,
minimumQueryLength,
});
const messageResults = React.useMemo(() => {
if (hasUnresolvedOperator) {
return [];
}
return dedupeSearchHits(searchQuery.data?.hits ?? []);
}, [hasUnresolvedOperator, searchQuery.data?.hits]);
return dedupeSearchHits([
...(prioritizedChannelSearchQuery.data?.hits ?? []),
...(searchQuery.data?.hits ?? []),
]);
}, [
hasUnresolvedOperator,
prioritizedChannelSearchQuery.data?.hits,
searchQuery.data?.hits,
]);
const channelResults = React.useMemo(() => {
if (scopeChannelId || ftsQuery.length < MIN_SEARCH_QUERY_LENGTH) {
return [];
@@ -498,6 +527,7 @@ export function useSearchResults({
isWaitingOnFromResolution: waitingOnFromResolution,
messageResults,
query,
prioritizedChannelSearchQuery,
resultProfiles: resultProfilesQuery.data?.profiles,
results,
searchQuery,

View File

@@ -441,9 +441,9 @@ test("Cmd+K bubbles unread conversations to the top and Enter opens the first",
page.getByTestId(`search-unread-count-${directMessageId}`),
).toHaveText("2");
await expect(firstUnread.getByText("Enter", { exact: true })).toBeVisible();
await expect(
page.getByTestId("search-current-channel-control"),
).toHaveAttribute("data-search-result-index", "1");
await expect(page.getByTestId("search-current-channel-control")).toHaveCount(
0,
);
await page.keyboard.press("Enter");
await expect(page).toHaveURL(
@@ -452,146 +452,29 @@ test("Cmd+K bubbles unread conversations to the top and Enter opens the first",
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
});
test("global search offers an optional current-channel scope", async ({
test("global search prioritizes messages from the current channel", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect(page).toHaveURL(
/#\/channels\/9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50$/,
);
await focusSidebarSearchWithShortcut(page);
const scopeControl = page.getByTestId("search-current-channel-control");
const input = page.getByTestId("search-dialog-input");
await expect
.poll(() =>
scopeControl
.getByTestId("search-current-scope-label")
.evaluate((element) => element.textContent),
)
.toBe("Search in #general");
await expect(scopeControl).toContainText("Search in");
await expect(scopeControl).toContainText("#general");
await expect(scopeControl).toContainText("Search messages in this channel");
await expect(page.getByTestId("search-dialog-input-row")).toHaveCSS(
"border-bottom-width",
"1px",
await expect(page.getByTestId("search-current-channel-control")).toHaveCount(
0,
);
await expect(scopeControl.locator("..")).toHaveCSS(
"border-bottom-width",
"0px",
);
await expect(scopeControl.locator("..")).toHaveCSS("padding-top", "14px");
await expect(scopeControl.locator("..")).toHaveCSS("padding-bottom", "14px");
const [controlBox, dialogBox] = await Promise.all([
scopeControl.boundingBox(),
page.getByTestId("search-results").boundingBox(),
]);
expect(controlBox).not.toBeNull();
expect(dialogBox).not.toBeNull();
expect(controlBox?.width ?? 0).toBeGreaterThan((dialogBox?.width ?? 0) * 0.9);
expect(controlBox?.width ?? 0).toBeLessThan(dialogBox?.width ?? 0);
await expect(scopeControl).toHaveAttribute("aria-selected", "true");
const firstRecentResult = page.locator(".search-result-row").first();
await input.press("ArrowDown");
await expect(firstRecentResult).toHaveAttribute("aria-selected", "true");
await input.press("ArrowUp");
await expect(scopeControl).toHaveAttribute("aria-selected", "true");
await input.press("Enter");
const scopeChip = page.getByTestId("search-channel-scope-chip");
await expect(scopeChip).toHaveText(/#general/);
await expect(input).toBeFocused();
await input.fill("w");
const relevantHeader = page.getByText("Most relevant", { exact: true });
const firstScopedResult = page
.locator('[data-search-section="messages"] .search-result-row')
.first();
await expect(page.getByText("Welcome to general")).toBeVisible();
await expect(page.getByText(/Searching messages in/)).toHaveCount(0);
await expect(relevantHeader).toBeVisible();
await expect(firstScopedResult).toBeVisible();
const contentStart = (element: HTMLElement) => {
const styles = window.getComputedStyle(element);
return (
element.getBoundingClientRect().left +
Number.parseFloat(styles.paddingLeft)
);
};
const [inputStart, headerStart, resultStart] = await Promise.all([
page.getByTestId("search-dialog-input-row").evaluate(contentStart),
relevantHeader.evaluate(contentStart),
firstScopedResult.evaluate(contentStart),
]);
expect(Math.abs(inputStart - headerStart)).toBeLessThanOrEqual(1);
expect(Math.abs(inputStart - resultStart)).toBeLessThanOrEqual(1);
await input.fill("x");
await expect(page.getByTestId("search-results")).toContainText(
"No messages for x in #general.",
);
await scopeChip.click();
await expect(scopeChip).toHaveCount(0);
await expect(input).toBeFocused();
await input.fill("shipped");
await expect(page.getByTestId("search-results")).toContainText(
"Engineering shipped the desktop build.",
);
});
test("global search offers a conversation-specific scope in direct messages", async ({
page,
}) => {
const directMessageId = "f48efb06-0c93-5025-aac9-2e646bb6bfa8";
await page.goto("/");
await page.getByTestId("channel-alice-tyler").click();
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
await expect(page).toHaveURL(
new RegExp(`#\\/channels\\/${directMessageId}$`),
);
await focusSidebarSearchWithShortcut(page);
const scopeControl = page.getByTestId("search-current-channel-control");
await expect
.poll(() =>
scopeControl
.getByTestId("search-current-scope-label")
.evaluate((element) => element.textContent),
)
.toBe("Search conversation with alice");
await expect(scopeControl).toContainText("Search conversation with alice");
await expect(scopeControl).toContainText(
"Search messages in this conversation.",
);
await expect(scopeControl).not.toContainText("channel");
await expect(page.getByTestId("search-dialog-input-row")).toHaveCSS(
"border-bottom-width",
"1px",
);
await expect(scopeControl.locator("..")).toHaveCSS(
"border-bottom-width",
"0px",
);
await expect(scopeControl.locator("..")).toHaveCSS("padding-top", "14px");
await expect(scopeControl.locator("..")).toHaveCSS("padding-bottom", "14px");
await scopeControl.click();
const scopeChip = page.getByTestId("search-channel-scope-chip");
const input = page.getByTestId("search-dialog-input");
await expect(scopeChip).toHaveText(/^alice$/);
await expect(scopeChip).not.toContainText("#");
await expect(input).toBeFocused();
await input.fill("a");
await expect(page.getByTestId("search-results")).toContainText(
"No messages for a in alice.",
await input.fill("welcome");
const currentChannelSection = page.locator(
'[data-search-section="current-channel-messages"]',
);
await expect(currentChannelSection).toContainText("In this conversation");
await expect(currentChannelSection).toContainText("Welcome to #general");
await expect(
currentChannelSection.locator(".search-result-row").first(),
).toHaveAttribute("aria-selected", "true");
await expect
.poll(() =>
page.evaluate(() => {
@@ -600,28 +483,27 @@ test("global search offers a conversation-specific scope in direct messages", as
window as Window & {
__BUZZ_E2E_COMMAND_LOG__?: Array<{
command: string;
payload: unknown;
payload: { channelId?: string; q?: string };
}>;
}
).__BUZZ_E2E_COMMAND_LOG__ ?? [];
return calls.findLast((entry) => entry.command === "search_messages")
?.payload;
return calls
.filter(
(entry) =>
entry.command === "search_messages" &&
entry.payload.q === "welcome",
)
.map((entry) => entry.payload.channelId ?? null);
}),
)
.toMatchObject({
channelId: directMessageId,
q: "a",
});
.toEqual(
expect.arrayContaining([null, "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"]),
);
await page.keyboard.press("Escape");
await page.keyboard.press("ControlOrMeta+f");
await expect(page.getByTestId("search-results")).toBeVisible();
await expect(page.getByTestId("search-channel-scope-chip")).toHaveText(
/^alice$/,
);
await expect(page.getByTestId("search-current-channel-control")).toHaveCount(
0,
await input.fill("shipped");
await expect(page.getByTestId("search-results")).toContainText(
"Engineering shipped the desktop build.",
);
});
@@ -710,15 +592,14 @@ test("global search exposes a larger scrollable result window", async ({
await page.getByTestId("search-dialog-input").fill("deep history message");
const resultRows = page.locator(
'[data-search-section="messages"] .search-result-row',
'[data-search-section="current-channel-messages"] .search-result-row',
);
await expect(resultRows).toHaveCount(40);
const resultList = page.getByTestId("search-results-list");
await expect(resultList).toBeVisible();
const scopeControl = page.getByTestId("search-current-channel-control");
await expect(
resultList.getByTestId("search-current-channel-control"),
).toBeVisible();
await expect(page.getByTestId("search-current-channel-control")).toHaveCount(
0,
);
const dimensions = await resultList.evaluate((element) => ({
clientHeight: element.clientHeight,
scrollHeight: element.scrollHeight,
@@ -727,12 +608,11 @@ test("global search exposes a larger scrollable result window", async ({
await resultList.evaluate((element) => {
element.scrollTop = element.scrollHeight;
});
await expect(scopeControl).not.toBeInViewport();
await resultList.evaluate((element) => {
element.scrollTop = 0;
});
for (let index = 0; index < 14; index += 1) {
for (let index = 0; index < 13; index += 1) {
await page.keyboard.press("ArrowDown");
}
await expect(resultRows.nth(13)).toHaveAttribute("aria-selected", "true");