mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): polish Projects navigation and detail presentation
Make sidebar and repository navigation behave predictably while clarifying review metadata, activity hierarchy, and file-detail presentation. Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
@@ -354,7 +354,7 @@ function DiscussionMessagePreview({
|
||||
}) {
|
||||
return (
|
||||
<Markdown
|
||||
className="inbox-preview-markdown mt-0.5 text-inherit leading-4"
|
||||
className="inbox-preview-markdown mt-0.5 text-inherit leading-6"
|
||||
content={discussionSnippet(content)}
|
||||
interactive={false}
|
||||
mentionNames={mentionNames}
|
||||
|
||||
@@ -15,12 +15,16 @@ export function CopyTextButton({
|
||||
text: string;
|
||||
}) {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const handleCopy = React.useCallback(() => {
|
||||
void writeTextToClipboard(text).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2_000);
|
||||
});
|
||||
}, [text]);
|
||||
const handleCopy = React.useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
void writeTextToClipboard(text).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2_000);
|
||||
});
|
||||
},
|
||||
[text],
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -30,6 +34,7 @@ export function CopyTextButton({
|
||||
className,
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
type="button"
|
||||
>
|
||||
{copied ? (
|
||||
|
||||
@@ -310,13 +310,13 @@ export function ProjectEntityListRow({
|
||||
</span>
|
||||
{count != null ? (
|
||||
<span
|
||||
className="grid w-12 shrink-0 grid-cols-[0.875rem_1fr] items-center gap-1 text-xs text-muted-foreground/65"
|
||||
className="flex w-12 shrink-0 items-center gap-1 text-xs text-muted-foreground/65"
|
||||
data-projects-text-priority="secondary"
|
||||
data-testid={countTestId}
|
||||
title={countTitle}
|
||||
>
|
||||
<MessageSquare className="h-3.5 w-3.5" />
|
||||
<span className="text-right tabular-nums">
|
||||
<span className="tabular-nums">
|
||||
{count}
|
||||
{countSuffix}
|
||||
</span>
|
||||
|
||||
@@ -531,6 +531,7 @@ export function ProjectRepositoryActionsPanel({
|
||||
<ProjectWorkItemContextDetails
|
||||
issue={selectedIssue}
|
||||
pullRequest={selectedPullRequest}
|
||||
repository={repository}
|
||||
/>
|
||||
{!selectedIssue &&
|
||||
!selectedPullRequest &&
|
||||
|
||||
@@ -620,6 +620,7 @@ export function RepositoryFilesPanel({
|
||||
profiles,
|
||||
fallbackAuthorPubkey,
|
||||
onContextChange,
|
||||
onOpenCommit,
|
||||
sourceControls,
|
||||
unavailableMessage,
|
||||
}: {
|
||||
@@ -633,6 +634,7 @@ export function RepositoryFilesPanel({
|
||||
kind: "file" | "folder";
|
||||
path: string;
|
||||
}) => void;
|
||||
onOpenCommit?: (commitHash: string) => void;
|
||||
/** Branch picker + remote/local toggle rendered in the panel header. */
|
||||
sourceControls?: RepoSourceHeaderControls;
|
||||
unavailableMessage?: string;
|
||||
@@ -838,9 +840,36 @@ export function RepositoryFilesPanel({
|
||||
) : null}
|
||||
|
||||
<div className="overflow-x-auto px-2 pb-2">
|
||||
<table className="w-full border-separate border-spacing-y-0.5 caption-bottom text-sm">
|
||||
<table className="w-full border-collapse caption-bottom text-sm">
|
||||
<thead>
|
||||
<tr className="border-border/50 border-b bg-muted/20">
|
||||
<tr
|
||||
aria-label={
|
||||
latestCommit
|
||||
? `Open commit ${latestCommit.shortHash}`
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
"bg-muted/20",
|
||||
latestCommit &&
|
||||
onOpenCommit &&
|
||||
"cursor-pointer transition-colors hover:bg-muted/35 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
||||
)}
|
||||
data-testid="project-repository-latest-commit"
|
||||
onClick={
|
||||
latestCommit && onOpenCommit
|
||||
? () => onOpenCommit(latestCommit.hash)
|
||||
: undefined
|
||||
}
|
||||
onKeyDown={
|
||||
latestCommit && onOpenCommit
|
||||
? (event) =>
|
||||
handleRepositoryEntryKeyDown(event, () =>
|
||||
onOpenCommit(latestCommit.hash),
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
tabIndex={latestCommit && onOpenCommit ? 0 : undefined}
|
||||
>
|
||||
<th className="px-4 py-3 text-left font-normal" colSpan={3}>
|
||||
{latestCommit ? (
|
||||
<div className="flex min-w-0 items-center justify-between gap-3 text-sm">
|
||||
@@ -892,9 +921,8 @@ export function RepositoryFilesPanel({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleEntries.map((entry, index) => {
|
||||
{visibleEntries.map((entry) => {
|
||||
const latestCommit = entry.latestCommit;
|
||||
const rowIsLast = index === visibleEntries.length - 1;
|
||||
const openEntry = () =>
|
||||
openRepositoryEntry(entry, setCurrentPath, setSelectedFile);
|
||||
|
||||
@@ -902,6 +930,7 @@ export function RepositoryFilesPanel({
|
||||
<tr
|
||||
aria-label={`Open ${entry.type} ${entry.name}`}
|
||||
className="group/repository-entry cursor-pointer text-xs focus-visible:outline-hidden"
|
||||
data-testid="project-repository-entry-row"
|
||||
key={`${entry.type}:${entry.path}`}
|
||||
onClick={openEntry}
|
||||
onKeyDown={(event) =>
|
||||
@@ -909,12 +938,7 @@ export function RepositoryFilesPanel({
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<td
|
||||
className={cn(
|
||||
"min-w-52 rounded-l-md px-3 py-2 align-middle transition-colors group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:bg-muted/35",
|
||||
!rowIsLast && "border-border/50 border-b",
|
||||
)}
|
||||
>
|
||||
<td className="min-w-52 px-3 py-2 align-middle transition-colors group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:bg-muted/35">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<RepositoryEntryIcon entry={entry} />
|
||||
<span className="truncate font-medium text-foreground">
|
||||
@@ -922,23 +946,13 @@ export function RepositoryFilesPanel({
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={cn(
|
||||
"max-w-96 p-2 align-middle transition-colors group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:bg-muted/35",
|
||||
!rowIsLast && "border-border/50 border-b",
|
||||
)}
|
||||
>
|
||||
<td className="max-w-96 p-2 align-middle transition-colors group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:bg-muted/35">
|
||||
<RepositoryCommitCell
|
||||
commit={latestCommit}
|
||||
profiles={profiles}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className={cn(
|
||||
"w-36 whitespace-nowrap rounded-r-md p-2 text-right align-middle text-muted-foreground transition-colors group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:bg-muted/35",
|
||||
!rowIsLast && "border-border/50 border-b",
|
||||
)}
|
||||
>
|
||||
<td className="w-36 whitespace-nowrap p-2 text-right align-middle text-muted-foreground transition-colors group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:bg-muted/35">
|
||||
{latestCommit ? (
|
||||
<time
|
||||
dateTime={new Date(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
CheckCircle2,
|
||||
CircleDot,
|
||||
GitBranch,
|
||||
GitPullRequest,
|
||||
type LucideIcon,
|
||||
MessageCircle,
|
||||
@@ -11,6 +12,7 @@ import type * as React from "react";
|
||||
import type {
|
||||
ProjectIssue,
|
||||
ProjectPullRequest,
|
||||
Repository,
|
||||
} from "@/features/projects/hooks";
|
||||
|
||||
function ContextDetailRow({
|
||||
@@ -36,9 +38,11 @@ function ContextDetailRow({
|
||||
export function ProjectWorkItemContextDetails({
|
||||
issue,
|
||||
pullRequest,
|
||||
repository,
|
||||
}: {
|
||||
issue?: ProjectIssue | null;
|
||||
pullRequest?: ProjectPullRequest | null;
|
||||
repository: Repository;
|
||||
}) {
|
||||
if (issue) {
|
||||
return (
|
||||
@@ -63,8 +67,25 @@ export function ProjectWorkItemContextDetails({
|
||||
}
|
||||
|
||||
if (pullRequest) {
|
||||
const sourceBranch = pullRequest.branchName || "Unknown branch";
|
||||
const targetBranch =
|
||||
pullRequest.targetBranch || repository.defaultBranch || "Default branch";
|
||||
const branchLabel = `${sourceBranch} → ${targetBranch}`;
|
||||
return (
|
||||
<>
|
||||
<ContextDetailRow
|
||||
icon={GitBranch}
|
||||
label="Branch"
|
||||
value={
|
||||
<span
|
||||
className="block max-w-40 truncate"
|
||||
data-testid="project-context-branch"
|
||||
title={branchLabel}
|
||||
>
|
||||
{branchLabel}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<ContextDetailRow
|
||||
icon={GitPullRequest}
|
||||
label="Status"
|
||||
|
||||
@@ -561,6 +561,7 @@ export function WorkspaceTabs({
|
||||
files={files}
|
||||
isLoading={displayedSnapshotLoading}
|
||||
onContextChange={onFilesContextChange}
|
||||
onOpenCommit={onSelectedCommitHashChange}
|
||||
profiles={profiles}
|
||||
snapshot={displayedSnapshot}
|
||||
unavailableMessage={
|
||||
|
||||
@@ -26,6 +26,8 @@ import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { ProjectsCreateMenu } from "./ProjectsCreateMenu";
|
||||
import { ProjectsSelectionCountMenu } from "./ProjectsSelectionCountMenu";
|
||||
import { useCommunities } from "@/features/communities/useCommunities";
|
||||
import { useActiveCommunityIcon } from "@/features/communities/useCommunityIcons";
|
||||
import {
|
||||
type OverviewContextStatIcon,
|
||||
type ProjectsOverviewSection,
|
||||
@@ -130,11 +132,34 @@ export function ProjectsOverviewPanel({
|
||||
}
|
||||
|
||||
export function ProjectsActivityIntro() {
|
||||
const { activeCommunity } = useCommunities();
|
||||
const communityIconQuery = useActiveCommunityIcon(activeCommunity?.relayUrl);
|
||||
const communityIcon = communityIconQuery.data ?? null;
|
||||
|
||||
return (
|
||||
<section
|
||||
className="pb-8 pt-5 text-center"
|
||||
className="pb-8 pt-16 text-center"
|
||||
data-testid="projects-activity-intro"
|
||||
>
|
||||
<div
|
||||
aria-label={`${activeCommunity?.name ?? "Current"} relay`}
|
||||
className="mx-auto mb-3 flex h-10 w-10 items-center justify-center overflow-hidden rounded-xl text-4xl"
|
||||
data-testid="projects-activity-relay-icon"
|
||||
role="img"
|
||||
>
|
||||
{communityIcon ? (
|
||||
<img
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
draggable={false}
|
||||
src={communityIcon}
|
||||
/>
|
||||
) : (
|
||||
<span aria-hidden="true" className="-translate-y-px leading-none">
|
||||
🐝
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h2
|
||||
className="text-xl font-semibold tracking-tight text-foreground"
|
||||
data-testid="projects-page-header"
|
||||
|
||||
@@ -291,7 +291,7 @@ export function PullRequestReviewersRow({
|
||||
<ProjectDetailMetaRow icon={Users} label="Reviewers">
|
||||
<div className="grid min-w-0 gap-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
||||
{showSummary ? (
|
||||
{showSummary && displayedDecisionActors.length === 0 ? (
|
||||
<span className="font-medium" data-testid={summaryTestId}>
|
||||
{reviewSummary}
|
||||
</span>
|
||||
@@ -317,32 +317,27 @@ export function PullRequestReviewersRow({
|
||||
? History
|
||||
: null;
|
||||
const decisionLabel = hasApproved
|
||||
? "Approved"
|
||||
? `Approved by ${label}`
|
||||
: hasRequestedChanges
|
||||
? "Changes requested"
|
||||
? `Changes requested by ${label}`
|
||||
: needsRereview
|
||||
? "Re-review needed"
|
||||
: "Pending";
|
||||
? `Re-review needed from ${label}`
|
||||
: `Awaiting review from ${label}`;
|
||||
return (
|
||||
<span
|
||||
className="flex min-w-0 items-center gap-2"
|
||||
className={cn(
|
||||
"flex min-w-0 items-center gap-1 text-sm",
|
||||
hasApproved && "text-green-600 dark:text-green-400",
|
||||
hasRequestedChanges && "text-amber-600 dark:text-amber-400",
|
||||
!hasApproved && !hasRequestedChanges && "text-muted-foreground",
|
||||
)}
|
||||
data-testid="project-reviewer-decision"
|
||||
key={pubkey}
|
||||
>
|
||||
<span className="truncate text-sm text-foreground">{label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex shrink-0 items-center gap-0.5 text-sm",
|
||||
hasApproved && "text-green-600 dark:text-green-400",
|
||||
hasRequestedChanges && "text-amber-600 dark:text-amber-400",
|
||||
!hasApproved &&
|
||||
!hasRequestedChanges &&
|
||||
"text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{DecisionIcon ? <DecisionIcon className="h-3.5 w-3.5" /> : null}
|
||||
{decisionLabel}
|
||||
</span>
|
||||
{DecisionIcon ? (
|
||||
<DecisionIcon className="h-3.5 w-3.5 shrink-0" />
|
||||
) : null}
|
||||
<span className="truncate">{decisionLabel}</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -32,7 +32,10 @@ import {
|
||||
useLeaveChannelDialog,
|
||||
type SectionDialogValue,
|
||||
} from "@/features/sidebar/ui/ChannelSectionDialogs";
|
||||
import { AppSidebarPinnedHeader } from "@/features/sidebar/ui/AppSidebarPinnedHeader";
|
||||
import {
|
||||
AppSidebarPinnedHeader,
|
||||
AppSidebarPrimaryMenu,
|
||||
} from "@/features/sidebar/ui/AppSidebarPinnedHeader";
|
||||
import { SidebarProjectsSection } from "@/features/sidebar/ui/SidebarProjectsSection";
|
||||
import { MoreUnreadButton } from "@/features/sidebar/ui/MoreUnreadButton";
|
||||
import { SidebarSection } from "@/features/sidebar/ui/SidebarSection";
|
||||
@@ -569,23 +572,15 @@ export function AppSidebar({
|
||||
currentChannelId={
|
||||
selectedView === "channel" ? selectedChannelId : null
|
||||
}
|
||||
homeBadgeCount={homeBadgeCount}
|
||||
onBrowseChannels={onBrowseChannels}
|
||||
onCreateAgent={onCreateAgent}
|
||||
onCreateChannel={handleOpenCreateChannel}
|
||||
onOpenDm={onOpenDm}
|
||||
onOpenSearchResult={onOpenSearchResult}
|
||||
onSelectAgents={onSelectAgents}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onSelectHome={onSelectHome}
|
||||
onSelectProjects={onSelectProjects}
|
||||
onSelectPulse={onSelectPulse}
|
||||
onSelectWorkflows={onSelectWorkflows}
|
||||
projectsOverviewActive={projectsOverviewActive}
|
||||
searchChannels={searchChannels}
|
||||
searchFocusRequest={searchFocusRequests[0]}
|
||||
scopeSearchFocusRequest={searchFocusRequests[1]}
|
||||
selectedView={selectedView}
|
||||
suggestionChannels={channels}
|
||||
/>
|
||||
|
||||
@@ -614,6 +609,9 @@ export function AppSidebar({
|
||||
data-sidebar-background
|
||||
data-testid="sidebar-scroll-content"
|
||||
>
|
||||
{/* biome-ignore format: keep compact to stay within file size limit */}
|
||||
<AppSidebarPrimaryMenu homeBadgeCount={homeBadgeCount} onSelectAgents={onSelectAgents} onSelectHome={onSelectHome} onSelectProjects={onSelectProjects} onSelectPulse={onSelectPulse} onSelectWorkflows={onSelectWorkflows} projectsOverviewActive={projectsOverviewActive} selectedView={selectedView} />
|
||||
|
||||
<SidebarProjectsSection />
|
||||
|
||||
{isLoading ? (
|
||||
|
||||
@@ -32,7 +32,7 @@ type AppSidebarPrimaryMenuProps = {
|
||||
selectedView: SidebarSelectedView;
|
||||
};
|
||||
|
||||
type AppSidebarPinnedHeaderProps = AppSidebarPrimaryMenuProps & {
|
||||
type AppSidebarPinnedHeaderProps = {
|
||||
channelLabels: Record<string, string>;
|
||||
currentChannelId?: string | null;
|
||||
currentPubkey?: string;
|
||||
@@ -52,23 +52,15 @@ export function AppSidebarPinnedHeader({
|
||||
channelLabels,
|
||||
currentChannelId,
|
||||
currentPubkey,
|
||||
homeBadgeCount,
|
||||
onBrowseChannels,
|
||||
onCreateAgent,
|
||||
onCreateChannel,
|
||||
onOpenDm,
|
||||
onOpenSearchResult,
|
||||
onSelectAgents,
|
||||
onSelectChannel,
|
||||
onSelectHome,
|
||||
onSelectProjects,
|
||||
onSelectPulse,
|
||||
onSelectWorkflows,
|
||||
projectsOverviewActive,
|
||||
searchChannels,
|
||||
searchFocusRequest,
|
||||
scopeSearchFocusRequest,
|
||||
selectedView,
|
||||
suggestionChannels,
|
||||
}: AppSidebarPinnedHeaderProps) {
|
||||
return (
|
||||
@@ -79,7 +71,6 @@ export function AppSidebarPinnedHeader({
|
||||
<TopbarSearch
|
||||
channelLabels={channelLabels}
|
||||
channels={searchChannels}
|
||||
className="mb-2"
|
||||
currentChannelId={currentChannelId}
|
||||
currentPubkey={currentPubkey}
|
||||
focusRequest={searchFocusRequest}
|
||||
@@ -92,16 +83,6 @@ export function AppSidebarPinnedHeader({
|
||||
scopeFocusRequest={scopeSearchFocusRequest}
|
||||
suggestionChannels={suggestionChannels}
|
||||
/>
|
||||
<AppSidebarPrimaryMenu
|
||||
homeBadgeCount={homeBadgeCount}
|
||||
onSelectAgents={onSelectAgents}
|
||||
onSelectHome={onSelectHome}
|
||||
onSelectProjects={onSelectProjects}
|
||||
onSelectPulse={onSelectPulse}
|
||||
onSelectWorkflows={onSelectWorkflows}
|
||||
projectsOverviewActive={projectsOverviewActive}
|
||||
selectedView={selectedView}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -118,7 +99,7 @@ export function AppSidebarPrimaryMenu({
|
||||
}: AppSidebarPrimaryMenuProps) {
|
||||
return (
|
||||
<SidebarHeader
|
||||
className="relative z-40 cursor-default select-none px-0 pb-2 pt-0"
|
||||
className="relative z-40 cursor-default select-none px-2 pb-2 pt-0"
|
||||
data-tauri-drag-region
|
||||
data-testid="sidebar-primary-menu"
|
||||
>
|
||||
|
||||
@@ -322,14 +322,7 @@ function SidebarProjectsSectionContent() {
|
||||
isActive={isActive}
|
||||
isExpanded={isExpanded}
|
||||
onDelete={() => setProjectToDelete(project)}
|
||||
onOpen={() => {
|
||||
if (isActive) {
|
||||
setProjectExpanded(project, !isExpanded);
|
||||
return;
|
||||
}
|
||||
setProjectExpanded(project, true);
|
||||
void goProject(project.id);
|
||||
}}
|
||||
onOpen={() => setProjectExpanded(project, !isExpanded)}
|
||||
onRemove={() => handleRemove(project)}
|
||||
project={project}
|
||||
/>
|
||||
|
||||
@@ -93,7 +93,7 @@ async function expectBuzzSidebarPalette(page: Page, mode: "light" | "dark") {
|
||||
(element) => getComputedStyle(element, "::before").backgroundColor,
|
||||
);
|
||||
expect(pinnedSpacerColor).toBe("rgba(0, 0, 0, 0)");
|
||||
await expect(pinnedHeader.getByTestId("open-agents-view")).toBeVisible();
|
||||
await expect(scrollContent.getByTestId("open-agents-view")).toBeVisible();
|
||||
const searchBox = await search.boundingBox();
|
||||
const pinnedHeaderBox = await pinnedHeader.boundingBox();
|
||||
const primaryMenuBox = await primaryMenu.boundingBox();
|
||||
@@ -129,7 +129,7 @@ async function expectBuzzSidebarPalette(page: Page, mode: "light" | "dark") {
|
||||
expect(searchBox.y + searchBox.height).toBeLessThanOrEqual(
|
||||
pinnedHeaderBox.y + pinnedHeaderBox.height,
|
||||
);
|
||||
expect(primaryMenuBox.y).toBeLessThan(
|
||||
expect(primaryMenuBox.y).toBeGreaterThanOrEqual(
|
||||
pinnedHeaderBox.y + pinnedHeaderBox.height,
|
||||
);
|
||||
for (const rowBox of [primaryRowBox, activeRowBox, hoverRowBox]) {
|
||||
|
||||
@@ -471,7 +471,13 @@ test("multi-repository projects switch the active repository", async ({
|
||||
},
|
||||
);
|
||||
await projectRow.click();
|
||||
await expect(page).toHaveURL(/\/projects\//);
|
||||
await expect(page).not.toHaveURL(/\/projects\//);
|
||||
await expect(projectRow).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(relayToolsRepository).toBeHidden();
|
||||
|
||||
await projectRow.click();
|
||||
await expect(page).not.toHaveURL(/\/projects\//);
|
||||
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(relayToolsRepository).toBeVisible();
|
||||
const projectSidebarMetrics = await sidebarScrollContent.evaluate(
|
||||
(element) => {
|
||||
@@ -497,7 +503,7 @@ test("multi-repository projects switch the active repository", async ({
|
||||
return element.scrollTop;
|
||||
});
|
||||
await projectRow.click();
|
||||
await expect(page).toHaveURL(/\/projects\//);
|
||||
await expect(page).not.toHaveURL(/\/projects\//);
|
||||
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -570,6 +576,30 @@ test("multi-repository projects switch the active repository", async ({
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test("latest files commit opens its detail without a divider", async ({
|
||||
page,
|
||||
}) => {
|
||||
await enableProjectsFeature(page);
|
||||
await installMockBridge(page);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByTestId("open-projects-view").click();
|
||||
await page.getByTestId("projects-section-projects").click();
|
||||
const projectEntry = page
|
||||
.locator(
|
||||
'[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]',
|
||||
)
|
||||
.first();
|
||||
await expect(projectEntry).toBeVisible({ timeout: 10_000 });
|
||||
await projectEntry.click();
|
||||
await page.getByRole("tab", { name: "Files" }).click();
|
||||
|
||||
const latestCommit = page.getByTestId("project-repository-latest-commit");
|
||||
await expect(latestCommit).toBeVisible();
|
||||
await expect(latestCommit).toHaveCSS("border-bottom-width", "0px");
|
||||
await latestCommit.click();
|
||||
await expect(page.getByTestId("project-commit-detail")).toBeVisible();
|
||||
});
|
||||
|
||||
test("commit detail opens from the commits feed with a diff", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -204,6 +204,7 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("project-context-chat-agent")).toBeVisible();
|
||||
await expect(page.getByTestId("project-context-discuss")).toBeVisible();
|
||||
await expect(page.getByTestId("project-context-branch")).toContainText("→");
|
||||
await expect(page.getByTestId("project-context-review-summary")).toHaveCount(
|
||||
0,
|
||||
);
|
||||
@@ -264,6 +265,12 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
|
||||
await expect(page.getByText("Requested a review from bob")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(page.getByTestId("project-review-summary")).toHaveCount(0);
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("project-reviewer-decision")
|
||||
.filter({ hasText: "Awaiting review from bob" }),
|
||||
).toHaveCount(1);
|
||||
|
||||
await waitForAnimations(page);
|
||||
await page.screenshot({
|
||||
@@ -287,9 +294,12 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
|
||||
.getByTestId("project-pull-request-timeline-row")
|
||||
.filter({ hasText: "requested changes" }),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByTestId("project-review-summary")).toHaveText(
|
||||
"Changes requested",
|
||||
);
|
||||
await expect(page.getByTestId("project-review-summary")).toHaveCount(0);
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("project-reviewer-decision")
|
||||
.filter({ hasText: "Changes requested" }),
|
||||
).toHaveCount(1);
|
||||
const changeRequestEvent = await page.evaluate(() =>
|
||||
window.__BUZZ_E2E_SIGNED_EVENTS__
|
||||
?.filter(
|
||||
@@ -368,10 +378,12 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Approve", exact: true }),
|
||||
).toHaveCount(0);
|
||||
await expect(page.getByTestId("project-review-summary")).toHaveText(
|
||||
"Awaiting review",
|
||||
);
|
||||
await expect(page.getByText("Approved", { exact: true })).toBeVisible();
|
||||
await expect(page.getByTestId("project-review-summary")).toHaveCount(0);
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("project-reviewer-decision")
|
||||
.filter({ hasText: "Approved by" }),
|
||||
).toHaveCount(1);
|
||||
const approvalEvent = await page.evaluate(() =>
|
||||
window.__BUZZ_E2E_SIGNED_EVENTS__
|
||||
?.filter(
|
||||
|
||||
@@ -40,7 +40,19 @@ test("projects activity overview screenshot", async ({ page }) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByTestId("open-projects-view").click();
|
||||
await expect(page.getByTestId("projects-page-tabs")).toBeVisible();
|
||||
await expect(page.getByTestId("projects-page-header")).toBeVisible();
|
||||
const activityHeader = page.getByTestId("projects-page-header");
|
||||
const relayIcon = page.getByTestId("projects-activity-relay-icon");
|
||||
await expect(activityHeader).toBeVisible();
|
||||
await expect(relayIcon).toBeVisible();
|
||||
const [activityHeaderBox, relayIconBox] = await Promise.all([
|
||||
activityHeader.boundingBox(),
|
||||
relayIcon.boundingBox(),
|
||||
]);
|
||||
expect(activityHeaderBox).not.toBeNull();
|
||||
expect(relayIconBox).not.toBeNull();
|
||||
expect((relayIconBox?.y ?? 0) + (relayIconBox?.height ?? 0)).toBeLessThan(
|
||||
activityHeaderBox?.y ?? 0,
|
||||
);
|
||||
await expect(page.getByTestId("projects-activity-search")).toBeVisible();
|
||||
await expect(page.getByTestId("projects-activity-intro")).toContainText(
|
||||
"Projects Activity",
|
||||
@@ -279,10 +291,10 @@ test("projects v3 workspace screenshot states", async ({ page }) => {
|
||||
name: "Fetch",
|
||||
exact: true,
|
||||
});
|
||||
await expect(peopleSection).toHaveCSS("border-top-width", "1px");
|
||||
await expect(peopleSection).toHaveCSS("border-top-width", "0px");
|
||||
await expect(detailsHeading.locator("..")).toHaveCSS(
|
||||
"border-top-width",
|
||||
"1px",
|
||||
"0px",
|
||||
);
|
||||
expect(
|
||||
await repositoryHeading.evaluate(
|
||||
@@ -305,9 +317,9 @@ test("projects v3 workspace screenshot states", async ({ page }) => {
|
||||
(peopleSectionBounds?.y ?? 0) -
|
||||
((actionsSectionBounds?.y ?? 0) + (actionsSectionBounds?.height ?? 0)),
|
||||
).toBe(8);
|
||||
expect(
|
||||
(detailsHeadingBounds?.y ?? 0) - (peopleSectionBounds?.y ?? 0) - 1,
|
||||
).toBe(8);
|
||||
expect((detailsHeadingBounds?.y ?? 0) - (peopleSectionBounds?.y ?? 0)).toBe(
|
||||
8,
|
||||
);
|
||||
const people = repositoryActionsPanel.getByTestId(
|
||||
"project-repository-person",
|
||||
);
|
||||
@@ -768,6 +780,13 @@ test("projects v3 workspace screenshot states", async ({ page }) => {
|
||||
await expect(
|
||||
workspacePanel.getByTestId("project-repository-entry-icon").first(),
|
||||
).toHaveCSS("border-radius", "8px");
|
||||
const repositoryEntryCell = workspacePanel
|
||||
.getByTestId("project-repository-entry-row")
|
||||
.first()
|
||||
.locator("td")
|
||||
.first();
|
||||
await expect(repositoryEntryCell).toHaveCSS("border-radius", "0px");
|
||||
await expect(repositoryEntryCell).toHaveCSS("border-bottom-width", "0px");
|
||||
await chatPanelTab.click();
|
||||
await expect(agentContext).toContainText("Files");
|
||||
await expect(
|
||||
@@ -861,7 +880,7 @@ test("projects v3 workspace screenshot states", async ({ page }) => {
|
||||
repositoryActionsPanel
|
||||
.getByTestId("project-repository-people")
|
||||
.locator(".."),
|
||||
).toHaveCSS("border-top-width", "1px");
|
||||
).toHaveCSS("border-top-width", "0px");
|
||||
await expect(
|
||||
repositoryActionsPanel.getByRole("heading", {
|
||||
name: "Task activity",
|
||||
|
||||
@@ -528,6 +528,47 @@ test("places sidebar search above the primary navigation", async ({ page }) => {
|
||||
expect(searchBox.y + searchBox.height).toBeLessThanOrEqual(menuBox.y);
|
||||
});
|
||||
|
||||
test("keeps only search pinned while primary navigation scrolls", async ({
|
||||
page,
|
||||
}) => {
|
||||
await loadTheme(page, "github-light");
|
||||
|
||||
const search = page.getByTestId("open-search");
|
||||
const primaryMenu = page.getByTestId("sidebar-primary-menu");
|
||||
const sidebarScroller = page.locator(".buzz-sidebar-scrollbar");
|
||||
const [initialSearchBox, initialMenuBox] = await Promise.all([
|
||||
search.boundingBox(),
|
||||
primaryMenu.boundingBox(),
|
||||
]);
|
||||
expect(initialSearchBox).not.toBeNull();
|
||||
expect(initialMenuBox).not.toBeNull();
|
||||
|
||||
const scrollTop = await sidebarScroller.evaluate((element) => {
|
||||
element.scrollTop = Math.min(
|
||||
120,
|
||||
Math.max(0, element.scrollHeight - element.clientHeight),
|
||||
);
|
||||
return element.scrollTop;
|
||||
});
|
||||
expect(scrollTop).toBeGreaterThan(0);
|
||||
await expect
|
||||
.poll(() =>
|
||||
sidebarScroller.evaluate((element) => Math.round(element.scrollTop)),
|
||||
)
|
||||
.toBe(Math.round(scrollTop));
|
||||
|
||||
const [scrolledSearchBox, scrolledMenuBox] = await Promise.all([
|
||||
search.boundingBox(),
|
||||
primaryMenu.boundingBox(),
|
||||
]);
|
||||
expect(scrolledSearchBox).not.toBeNull();
|
||||
expect(scrolledMenuBox).not.toBeNull();
|
||||
expect(
|
||||
Math.abs((scrolledSearchBox?.y ?? 0) - (initialSearchBox?.y ?? 0)),
|
||||
).toBeLessThanOrEqual(1);
|
||||
expect(scrolledMenuBox?.y ?? 0).toBeLessThan(initialMenuBox?.y ?? 0);
|
||||
});
|
||||
|
||||
test("sidebar rail resizes without toggling the sidebar", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const rail = page.getByRole("button", { name: "Resize sidebar" });
|
||||
|
||||
Reference in New Issue
Block a user