mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
Move Pulse app navigation to the window sidebar
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
@@ -103,6 +103,7 @@ export type { ProfilePanelTab, ProfilePanelView };
|
||||
|
||||
export function UserProfilePanel({
|
||||
canResetWidth,
|
||||
className,
|
||||
currentPubkey,
|
||||
isSinglePanelView = false,
|
||||
layout = "standalone",
|
||||
@@ -976,6 +977,7 @@ export function UserProfilePanel({
|
||||
<UserProfilePanelFrame
|
||||
addAgentToChannelDialog={addAgentToChannelDialog}
|
||||
canResetWidth={canResetWidth}
|
||||
className={className}
|
||||
editAgentDialog={editAgentDialog}
|
||||
headerActions={headerActions}
|
||||
headerLeftContent={headerLeftContent}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { cn } from "@/shared/lib/cn";
|
||||
type UserProfilePanelFrameProps = {
|
||||
addAgentToChannelDialog: React.ReactNode;
|
||||
canResetWidth?: boolean;
|
||||
className?: string;
|
||||
editAgentDialog: React.ReactNode;
|
||||
headerActions: React.ReactNode;
|
||||
headerLeftContent: React.ReactNode;
|
||||
@@ -32,6 +33,7 @@ type UserProfilePanelFrameProps = {
|
||||
export function UserProfilePanelFrame({
|
||||
addAgentToChannelDialog,
|
||||
canResetWidth,
|
||||
className,
|
||||
editAgentDialog,
|
||||
headerActions,
|
||||
headerLeftContent,
|
||||
@@ -53,7 +55,7 @@ export function UserProfilePanelFrame({
|
||||
return (
|
||||
<AuxiliaryPanel
|
||||
canResetWidth={canResetWidth}
|
||||
className="relative"
|
||||
className={cn("relative", className)}
|
||||
isSinglePanelView={isSinglePanelView}
|
||||
layout={isSplitLayout ? "split" : "standalone"}
|
||||
onClose={onClose}
|
||||
|
||||
@@ -97,6 +97,8 @@ export function profilePanelTargetKey(
|
||||
|
||||
export type UserProfilePanelProps = {
|
||||
canResetWidth?: boolean;
|
||||
/** Surface styling supplied by the host layout. */
|
||||
className?: string;
|
||||
currentPubkey?: string;
|
||||
isSinglePanelView?: boolean;
|
||||
layout?: "standalone" | "split";
|
||||
|
||||
51
desktop/src/features/pulse/ui/PulseAppNavigation.tsx
Normal file
51
desktop/src/features/pulse/ui/PulseAppNavigation.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Bot, Folders, MessageCircle, Zap } from "lucide-react";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import type { PulseWorkspacePage } from "../lib/workspaceNavigation";
|
||||
|
||||
export type PulseApp = "messages" | PulseWorkspacePage;
|
||||
|
||||
const apps = {
|
||||
messages: { label: "Messages", icon: MessageCircle },
|
||||
projects: { label: "Projects", icon: Folders },
|
||||
agents: { label: "Agents", icon: Bot },
|
||||
workflows: { label: "Workflows", icon: Zap },
|
||||
} as const;
|
||||
|
||||
/** Window-level app switching, separate from each app's own navigation. */
|
||||
export function PulseAppNavigation({
|
||||
active,
|
||||
onSelect,
|
||||
}: {
|
||||
active: PulseApp;
|
||||
onSelect: (app: PulseApp) => void;
|
||||
}) {
|
||||
return (
|
||||
<nav
|
||||
aria-label="Apps"
|
||||
data-testid="pulse-app-navigation"
|
||||
className="w-[64px] shrink-0 space-y-1 overflow-y-auto px-2 py-2 lg:w-[180px]"
|
||||
>
|
||||
{(Object.keys(apps) as PulseApp[]).map((app) => {
|
||||
const Icon = apps[app].icon;
|
||||
return (
|
||||
<button
|
||||
key={app}
|
||||
type="button"
|
||||
aria-label={apps[app].label}
|
||||
aria-current={active === app ? "page" : undefined}
|
||||
onClick={() => onSelect(app)}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-center gap-3 rounded-xl px-3 py-3 text-left text-sm hover:bg-muted/35 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring lg:justify-start",
|
||||
active === app
|
||||
? "font-semibold text-foreground"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon aria-hidden="true" className="h-4 w-4 shrink-0" />
|
||||
<span className="hidden lg:inline">{apps[app].label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,6 @@ export function PulseCombinedView({
|
||||
scrollRef,
|
||||
children,
|
||||
view,
|
||||
workspaceContent,
|
||||
onSelectView,
|
||||
}: {
|
||||
grouped?: boolean;
|
||||
@@ -23,7 +22,6 @@ export function PulseCombinedView({
|
||||
scrollRef: React.RefCallback<HTMLDivElement>;
|
||||
children: React.ReactNode;
|
||||
view: PulseView;
|
||||
workspaceContent?: React.ReactNode;
|
||||
onSelectView: (view: PulseView) => void;
|
||||
}) {
|
||||
const sorted = React.useMemo(() => {
|
||||
@@ -49,7 +47,6 @@ export function PulseCombinedView({
|
||||
testPrefix="pulse-combined"
|
||||
allMessages={{ content: children, scrollRef }}
|
||||
navigation={{ view, onSelectView }}
|
||||
workspaceContent={workspaceContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,15 +6,7 @@ import {
|
||||
CLEAR_WORKSPACE_PANELS,
|
||||
type PulseView,
|
||||
} from "../lib/workspaceNavigation";
|
||||
import {
|
||||
Inbox,
|
||||
Search,
|
||||
MessageCircle,
|
||||
Users,
|
||||
Folders,
|
||||
Bot,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { Inbox, Search, MessageCircle, Users } from "lucide-react";
|
||||
import { allowNavigation } from "@/app/navigation/navigationGuard";
|
||||
import { buildDirectMessageIntro } from "@/features/channels/lib/dmParticipantDisplay";
|
||||
import { useUsersBatchQuery } from "@/features/profile/hooks";
|
||||
@@ -62,7 +54,6 @@ export function PulseConversationSplitView({
|
||||
testPrefix,
|
||||
allMessages,
|
||||
navigation,
|
||||
workspaceContent,
|
||||
}: {
|
||||
grouped?: boolean;
|
||||
channels: Channel[];
|
||||
@@ -75,7 +66,6 @@ export function PulseConversationSplitView({
|
||||
content: React.ReactNode;
|
||||
scrollRef: React.RefCallback<HTMLDivElement>;
|
||||
};
|
||||
workspaceContent?: React.ReactNode;
|
||||
navigation?: {
|
||||
view: PulseView;
|
||||
onSelectView: (view: PulseView) => void;
|
||||
@@ -176,9 +166,6 @@ export function PulseConversationSplitView({
|
||||
[
|
||||
{ id: "search", label: "Search", icon: Search },
|
||||
{ id: "all", label: "For you", icon: Inbox },
|
||||
{ id: "projects", label: "Projects", icon: Folders },
|
||||
{ id: "agents", label: "Agents", icon: Bot },
|
||||
{ id: "workflows", label: "Workflows", icon: Zap },
|
||||
] as const
|
||||
).map(({ id, label, icon: Icon }) => (
|
||||
<button
|
||||
@@ -308,37 +295,36 @@ export function PulseConversationSplitView({
|
||||
</p>
|
||||
)}
|
||||
</nav>
|
||||
{workspaceContent ??
|
||||
(!selected && allMessages ? (
|
||||
<div
|
||||
ref={allMessages.scrollRef}
|
||||
className="min-h-0 min-w-0 flex-1 overflow-y-auto"
|
||||
data-testid={
|
||||
navigation?.view === "search"
|
||||
? "pulse-search-feed"
|
||||
: navigation?.view === "all"
|
||||
? "pulse-briefing-feed"
|
||||
: "pulse-all-messages-feed"
|
||||
}
|
||||
>
|
||||
{allMessages.content}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"
|
||||
data-testid={`${testPrefix}-detail`}
|
||||
data-channel-id={selected?.id}
|
||||
>
|
||||
{selected ? (
|
||||
<PulseChannelDetail channel={selected} />
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-3 text-sm text-muted-foreground">
|
||||
<MessageCircle aria-hidden className="h-6 w-6" />
|
||||
<p>Select a conversation</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!selected && allMessages ? (
|
||||
<div
|
||||
ref={allMessages.scrollRef}
|
||||
className="min-h-0 min-w-0 flex-1 overflow-y-auto"
|
||||
data-testid={
|
||||
navigation?.view === "search"
|
||||
? "pulse-search-feed"
|
||||
: navigation?.view === "all"
|
||||
? "pulse-briefing-feed"
|
||||
: "pulse-all-messages-feed"
|
||||
}
|
||||
>
|
||||
{allMessages.content}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"
|
||||
data-testid={`${testPrefix}-detail`}
|
||||
data-channel-id={selected?.id}
|
||||
>
|
||||
{selected ? (
|
||||
<PulseChannelDetail channel={selected} />
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-3 text-sm text-muted-foreground">
|
||||
<MessageCircle aria-hidden className="h-6 w-6" />
|
||||
<p>Select a conversation</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ export function PulseScreen() {
|
||||
!(values.feed === "channel" && values.channel) ? (
|
||||
<UserProfilePanel
|
||||
canResetWidth={threadPanelWidth.canReset}
|
||||
className="my-[12px] mr-[8px] h-auto overflow-hidden rounded-[24px] border-0"
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
onClose={handleCloseProfilePanel}
|
||||
onOpenDm={handleOpenDm}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
PULSE_WORKSPACE_KEYS,
|
||||
CLEAR_WORKSPACE_PANELS,
|
||||
} from "../lib/workspaceNavigation";
|
||||
import { PulseAppNavigation, type PulseApp } from "./PulseAppNavigation";
|
||||
import { PulseWorkspacePage } from "./PulseWorkspacePage";
|
||||
import { PulseCombinedView } from "./PulseCombinedView";
|
||||
import { PulseVariationMenu } from "./PulseVariationMenu";
|
||||
@@ -92,6 +93,19 @@ export function UnifiedPulseView({
|
||||
setBriefingFilter(null);
|
||||
return true;
|
||||
};
|
||||
const activeApp: PulseApp = isPulseWorkspacePage(filter)
|
||||
? filter
|
||||
: "messages";
|
||||
const lastMessageView = React.useRef<PulseView>(
|
||||
values.conversation ? "conversation" : "all",
|
||||
);
|
||||
React.useEffect(() => {
|
||||
if (!isPulseWorkspacePage(filter)) lastMessageView.current = filter;
|
||||
}, [filter]);
|
||||
const selectApp = (app: PulseApp) => {
|
||||
if (app === activeApp) return;
|
||||
setFilter(app === "messages" ? lastMessageView.current : app);
|
||||
};
|
||||
const setVariation = (next: "separate" | "combined") => {
|
||||
if ((next === "combined") === combined) return;
|
||||
if (!allowNavigation({ kind: "route", href: `/pulse?layout=${next}` }))
|
||||
@@ -332,56 +346,58 @@ export function UnifiedPulseView({
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col py-[12px]",
|
||||
expanded && "px-[8px]",
|
||||
)}
|
||||
className="flex min-h-0 flex-1 gap-2 px-[8px] py-[12px]"
|
||||
data-testid="unified-pulse"
|
||||
>
|
||||
<div
|
||||
data-testid="pulse-main-container"
|
||||
className={cn(
|
||||
"relative mx-auto flex min-h-0 w-full flex-1 flex-col overflow-hidden rounded-[24px] bg-background",
|
||||
!expanded && "max-w-[960px]",
|
||||
)}
|
||||
data-expanded={expanded}
|
||||
>
|
||||
<PulseVariationMenu
|
||||
value={combined ? "combined" : "separate"}
|
||||
onChange={setVariation}
|
||||
onRefresh={() => void feed.refresh()}
|
||||
refreshing={feed.query.isFetching}
|
||||
/>
|
||||
<div className="pulse-conversation-workspace flex min-h-0 flex-1">
|
||||
<div
|
||||
className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"
|
||||
data-testid="pulse-scroll-area"
|
||||
>
|
||||
<PulseCombinedView
|
||||
grouped={!combined}
|
||||
channels={feed.channels}
|
||||
conversations={feed.conversations}
|
||||
currentPubkey={currentPubkey}
|
||||
scrollRef={setScrollElement}
|
||||
view={filter}
|
||||
workspaceContent={
|
||||
isPulseWorkspacePage(filter) ? (
|
||||
<PulseWorkspacePage page={filter} />
|
||||
) : undefined
|
||||
}
|
||||
onSelectView={setFilter}
|
||||
<PulseAppNavigation active={activeApp} onSelect={selectApp} />
|
||||
<div className="flex min-h-0 min-w-0 flex-1">
|
||||
<div
|
||||
data-testid="pulse-main-container"
|
||||
className={cn(
|
||||
"relative mx-auto flex min-h-0 w-full flex-1 flex-col overflow-hidden rounded-[24px] bg-background",
|
||||
!expanded && "max-w-[960px]",
|
||||
)}
|
||||
data-expanded={expanded}
|
||||
>
|
||||
<PulseVariationMenu
|
||||
value={combined ? "combined" : "separate"}
|
||||
onChange={setVariation}
|
||||
onRefresh={() => void feed.refresh()}
|
||||
refreshing={feed.query.isFetching}
|
||||
/>
|
||||
<div className="pulse-conversation-workspace flex min-h-0 flex-1">
|
||||
<div
|
||||
className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"
|
||||
data-testid="pulse-scroll-area"
|
||||
>
|
||||
{content}
|
||||
</PulseCombinedView>
|
||||
</div>
|
||||
<div
|
||||
className="pulse-terminal-side-host"
|
||||
data-testid="pulse-terminal-panel"
|
||||
>
|
||||
{terminal}
|
||||
{isPulseWorkspacePage(filter) ? (
|
||||
<PulseWorkspacePage page={filter} />
|
||||
) : (
|
||||
<PulseCombinedView
|
||||
grouped={!combined}
|
||||
channels={feed.channels}
|
||||
conversations={feed.conversations}
|
||||
currentPubkey={currentPubkey}
|
||||
scrollRef={setScrollElement}
|
||||
view={filter}
|
||||
onSelectView={setFilter}
|
||||
>
|
||||
{content}
|
||||
</PulseCombinedView>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="pulse-terminal-side-host"
|
||||
data-testid="pulse-terminal-panel"
|
||||
>
|
||||
{terminal}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!expanded && (
|
||||
<div aria-hidden="true" className="w-[64px] shrink-0 lg:w-[180px]" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -584,7 +584,7 @@ test("channel rail opens conversations and preserves drafts", async ({
|
||||
await expect(list).toHaveCSS("width", "220px");
|
||||
const all = list.getByRole("button", { name: "All messages", exact: true });
|
||||
const aggregate = page.getByTestId("pulse-all-messages-feed");
|
||||
await expect(list.getByRole("button").nth(5)).toHaveText("All messages");
|
||||
await expect(list.getByRole("button").nth(2)).toHaveText("All messages");
|
||||
await expect(all).toHaveAttribute("aria-current", "true");
|
||||
await expect(
|
||||
aggregate
|
||||
@@ -710,7 +710,7 @@ test("app variations combine conversations by recency and preserve selection and
|
||||
await expect(page.getByTestId("pulse-tabs")).toHaveCount(0);
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("pulse-combined-list")
|
||||
.getByTestId("pulse-app-navigation")
|
||||
.getByRole("button", { name: "Agents", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
@@ -724,7 +724,7 @@ test("app variations combine conversations by recency and preserve selection and
|
||||
const aggregate = page.getByTestId("pulse-all-messages-feed");
|
||||
await expect(list.getByRole("button").nth(0)).toHaveText("Search");
|
||||
await expect(list.getByRole("button").nth(1)).toHaveText("For you");
|
||||
await expect(list.getByRole("button").nth(5)).toHaveText("All messages");
|
||||
await expect(list.getByRole("button").nth(2)).toHaveText("All messages");
|
||||
await list.getByRole("button", { name: "Search", exact: true }).click();
|
||||
await expect(
|
||||
page.getByRole("searchbox", { name: "Search loaded feed" }),
|
||||
@@ -1347,7 +1347,7 @@ test("side panels expand Pulse and terminal docks beside the conversation", asyn
|
||||
await expect(container).toHaveCSS("max-width", "none");
|
||||
await expect
|
||||
.poll(async () => (await boundsOf(container)).width)
|
||||
.toBeGreaterThan(1400);
|
||||
.toBeGreaterThan(1300);
|
||||
const conversationBounds = await boundsOf(detail);
|
||||
const terminalBounds = await boundsOf(terminal);
|
||||
expect(terminalBounds.x).toBeGreaterThanOrEqual(
|
||||
@@ -1355,8 +1355,9 @@ test("side panels expand Pulse and terminal docks beside the conversation", asyn
|
||||
);
|
||||
expect(terminalBounds.height).toBeGreaterThan(700);
|
||||
const expandedBounds = await boundsOf(container);
|
||||
expect(expandedBounds.x).toBe(8);
|
||||
expect(expandedBounds.width).toBe(1584);
|
||||
const appsBounds = await boundsOf(page.getByTestId("pulse-app-navigation"));
|
||||
expect(expandedBounds.x).toBe(appsBounds.x + appsBounds.width + 8);
|
||||
expect(expandedBounds.x + expandedBounds.width).toBe(1592);
|
||||
const chatHeader = detail.getByTestId("chat-header");
|
||||
const terminalHeader = terminal.getByTestId("terminal-header");
|
||||
expect((await boundsOf(terminalHeader)).height).toBe(
|
||||
@@ -1817,6 +1818,20 @@ test("workspace entrypoints keep projects, agents, and workflows in the main pan
|
||||
await seed(page);
|
||||
const rail = page.getByTestId("pulse-combined-list");
|
||||
const main = page.getByTestId("pulse-main-container");
|
||||
const apps = page.getByTestId("pulse-app-navigation");
|
||||
await expect(main.getByTestId("pulse-app-heading")).toHaveCount(0);
|
||||
await expect(
|
||||
apps.getByRole("button", { name: "Messages", exact: true }),
|
||||
).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
await expect(apps.locator("button > svg")).toHaveCount(4);
|
||||
for (const width of [1600, 1280, 900]) {
|
||||
await page.setViewportSize({ width, height: 960 });
|
||||
const box = await boundsOf(main);
|
||||
expect(box.x + box.width / 2).toBeCloseTo(width / 2, 0);
|
||||
const navBox = await boundsOf(apps);
|
||||
expect(box.x).toBeGreaterThanOrEqual(navBox.x + navBox.width);
|
||||
}
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
await expect(
|
||||
rail
|
||||
.getByRole("button")
|
||||
@@ -1824,8 +1839,16 @@ test("workspace entrypoints keep projects, agents, and workflows in the main pan
|
||||
.first(),
|
||||
).toHaveText("Search");
|
||||
await expect(rail.getByRole("button").nth(1)).toHaveText("For you");
|
||||
for (const [index, name] of ["Projects", "Agents", "Workflows"].entries()) {
|
||||
await expect(rail.getByRole("button").nth(index + 2)).toHaveText(name);
|
||||
await expect(apps.getByRole("button")).toHaveText([
|
||||
"Messages",
|
||||
"Projects",
|
||||
"Agents",
|
||||
"Workflows",
|
||||
]);
|
||||
for (const name of ["Projects", "Agents", "Workflows"]) {
|
||||
await expect(rail.getByRole("button", { name, exact: true })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
}
|
||||
for (const variation of ["Combined conversations", "Separate feeds"]) {
|
||||
await page
|
||||
@@ -1839,24 +1862,29 @@ test("workspace entrypoints keep projects, agents, and workflows in the main pan
|
||||
["Agents", "agents-page-content"],
|
||||
["Workflows", "workflows-view"],
|
||||
]) {
|
||||
const entry = rail.getByRole("button", { name, exact: true });
|
||||
const entry = apps.getByRole("button", { name, exact: true });
|
||||
await entry.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
const panel = main.getByTestId(`pulse-workspace-${name.toLowerCase()}`);
|
||||
await expect(panel.getByTestId(contentTestId)).toBeVisible();
|
||||
await expect(entry).toHaveAttribute("aria-current", "true");
|
||||
await expect(rail).toBeVisible();
|
||||
await expect(entry).toHaveAttribute("aria-current", "page");
|
||||
await expect(apps).toBeVisible();
|
||||
await expect(main).toHaveCSS("max-width", "960px");
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(`#/pulse\\?.*feed=${name.toLowerCase()}`),
|
||||
);
|
||||
await expect(page.getByTestId("open-pulse-view")).toHaveCount(0);
|
||||
const railBox = await boundsOf(rail);
|
||||
await expect(rail).toHaveCount(0);
|
||||
await expect(main.getByTestId("pulse-app-heading")).toHaveCount(0);
|
||||
const appBox = await boundsOf(apps);
|
||||
const mainBox = await boundsOf(main);
|
||||
const panelBox = await boundsOf(panel);
|
||||
expect(panelBox.x).toBeCloseTo(railBox.x + railBox.width, 0);
|
||||
expect(mainBox.x).toBeGreaterThanOrEqual(appBox.x + appBox.width + 8);
|
||||
expect(panelBox.width).toBeCloseTo(mainBox.width, 0);
|
||||
expect(mainBox.x + mainBox.width / 2).toBeCloseTo(720, 0);
|
||||
await page.reload();
|
||||
await expect(panel.getByTestId(contentTestId)).toBeVisible();
|
||||
await expect(entry).toHaveAttribute("aria-current", "true");
|
||||
await expect(entry).toHaveAttribute("aria-current", "page");
|
||||
}
|
||||
}
|
||||
await main
|
||||
@@ -1865,11 +1893,11 @@ test("workspace entrypoints keep projects, agents, and workflows in the main pan
|
||||
const editor = page.getByRole("dialog", { name: "Create workflow" });
|
||||
await expect(editor).toBeVisible();
|
||||
await expect(page).toHaveURL(/#\/pulse\?.*view=create/);
|
||||
await expect(rail).toBeVisible();
|
||||
await expect(apps).toBeVisible();
|
||||
await page.goBack();
|
||||
await expect(editor).toHaveCount(0);
|
||||
await expect(main.getByTestId("workflows-view")).toBeVisible();
|
||||
await rail.getByRole("button", { name: "Projects", exact: true }).click();
|
||||
await apps.getByRole("button", { name: "Projects", exact: true }).click();
|
||||
await main.getByTestId("projects-section-projects").click();
|
||||
const project = main.locator(
|
||||
'[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]',
|
||||
@@ -1878,20 +1906,32 @@ test("workspace entrypoints keep projects, agents, and workflows in the main pan
|
||||
await project.click();
|
||||
await expect(page).toHaveURL(/#\/pulse\?.*projectId=/);
|
||||
await expect(main.getByTestId("pulse-workspace-projects")).toBeVisible();
|
||||
await expect(rail).toBeVisible();
|
||||
await expect(apps).toBeVisible();
|
||||
await page.goBack();
|
||||
await main.getByTestId("projects-section-projects").click();
|
||||
await expect(project).toBeVisible();
|
||||
await apps.getByRole("button", { name: "Messages", exact: true }).click();
|
||||
await expect(rail).toBeVisible();
|
||||
await rail.locator('[data-channel-name="bob-tyler"]').click();
|
||||
await expect(main.getByTestId("message-input")).toBeVisible();
|
||||
await expect(main.getByTestId("pulse-workspace-projects")).toHaveCount(0);
|
||||
const composer = main.getByTestId("message-input");
|
||||
await composer.fill("Keep this draft while switching apps");
|
||||
await apps.getByRole("button", { name: "Agents", exact: true }).click();
|
||||
await expect(rail).toHaveCount(0);
|
||||
await apps.getByRole("button", { name: "Messages", exact: true }).click();
|
||||
await expect(composer).toHaveText("Keep this draft while switching apps");
|
||||
await waitForAnimations(page);
|
||||
await page
|
||||
.getByTestId("unified-pulse")
|
||||
.screenshot({ path: "test-results/pulse-prototype/apps-messages.png" });
|
||||
await rail.getByRole("button", { name: "For you", exact: true }).click();
|
||||
await expect(main.getByTestId("pulse-briefing")).toBeVisible();
|
||||
await rail.getByRole("button", { name: "Projects", exact: true }).click();
|
||||
await apps.getByRole("button", { name: "Projects", exact: true }).click();
|
||||
await main.getByTestId("projects-section-projects").click();
|
||||
await expect(project).toBeVisible();
|
||||
await waitForAnimations(page);
|
||||
await main.screenshot({
|
||||
await page.getByTestId("unified-pulse").screenshot({
|
||||
path: "test-results/pulse-prototype/workspace-projects.png",
|
||||
});
|
||||
});
|
||||
@@ -2141,3 +2181,54 @@ test("agent conversation rows replace unread dots with notification-sized workin
|
||||
await expect(working).toHaveCount(0);
|
||||
await expect(unread).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("workspace profile panels share the main container's frame and remain resizable", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seed(page);
|
||||
await page
|
||||
.getByTestId("pulse-app-navigation")
|
||||
.getByRole("button", { name: "Projects", exact: true })
|
||||
.click();
|
||||
// Exercise a profile deep link through the real Pulse route and profile panel.
|
||||
await page.evaluate((pubkey) => {
|
||||
const [path, query] = location.hash.split("?");
|
||||
const params = new URLSearchParams(query);
|
||||
params.set("profile", pubkey);
|
||||
location.hash = `${path}?${params}`;
|
||||
}, TEST_IDENTITIES.bob.pubkey);
|
||||
const panel = page.getByTestId("user-profile-panel");
|
||||
const main = page.getByTestId("pulse-main-container");
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(panel).toHaveCSS("border-radius", "24px");
|
||||
await expect(panel).toHaveCSS("border-width", "0px");
|
||||
await expect(main).toHaveAttribute("data-expanded", "true");
|
||||
await waitForAnimations(page);
|
||||
const panelBox = await boundsOf(panel);
|
||||
const mainBox = await boundsOf(main);
|
||||
expect(panelBox.y).toBe(mainBox.y);
|
||||
expect(panelBox.height).toBe(mainBox.height);
|
||||
expect(panelBox.x - mainBox.x - mainBox.width).toBeCloseTo(8, 0);
|
||||
expect(panelBox.x + panelBox.width).toBe(1432);
|
||||
await page.screenshot({
|
||||
path: "test-results/pulse-prototype/workspace-profile-frame.png",
|
||||
});
|
||||
const handle = await boundsOf(
|
||||
panel.getByTestId("user-profile-resize-handle"),
|
||||
);
|
||||
// Use the part of the resize target inside the rounded surface.
|
||||
const x = panelBox.x + 2;
|
||||
const y = handle.y + handle.height / 2;
|
||||
await page.mouse.move(x, y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(x - 40, y, { steps: 5 });
|
||||
await page.mouse.up();
|
||||
await expect
|
||||
.poll(async () => (await boundsOf(panel)).width)
|
||||
.toBeGreaterThan(panelBox.width + 20);
|
||||
await panel.getByRole("button", { name: "Close panel", exact: true }).click();
|
||||
await expect(panel).toHaveCount(0);
|
||||
await expect(main).toHaveCSS("max-width", "960px");
|
||||
const restored = await boundsOf(main);
|
||||
expect(restored.x + restored.width / 2).toBeCloseTo(720, 0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user