fix: allow unlinking chat workspaces (#25833)

This allows a Coder Agents chat to detach from its linked workspace
without deleting or changing the workspace, so a different workspace can
be linked later. It adds detach controls wherever the linked workspace
appears, including the workspace pill menu, fallback workspace badges,
and the workspace picker. The workspace selection state now updates
consistently across desktop and mobile.

Running workspace:
<img width="453" height="296" alt="image"
src="https://github.com/user-attachments/assets/ac5197a7-f0f4-4123-bbea-d3ddaca7a3e4"
/>

Stopped workspace:
<img width="389" height="203" alt="image"
src="https://github.com/user-attachments/assets/f5a8a90c-4bb0-405a-ade3-791146687b2d"
/>


Closes CODAGT-510
This commit is contained in:
Ethan
2026-06-02 14:40:07 +10:00
committed by GitHub
parent 97dde1f824
commit 49c2142d2d
12 changed files with 291 additions and 88 deletions
+3 -3
View File
@@ -129,7 +129,7 @@ describe("getAppHref", () => {
path: "/path-base",
});
expect(href).toBe(
`/path-base/@${MockWorkspace.owner_name}/Test-Workspace.a-workspace-agent/apps/${app.slug}/`,
`/path-base/@${MockWorkspace.owner_name}/test-workspace.a-workspace-agent/apps/${app.slug}/`,
);
});
@@ -145,7 +145,7 @@ describe("getAppHref", () => {
path: "",
});
expect(href).toBe(
`/@${MockWorkspace.owner_name}/Test-Workspace.a-workspace-agent/terminal?app=${app.slug}`,
`/@${MockWorkspace.owner_name}/test-workspace.a-workspace-agent/terminal?app=${app.slug}`,
);
});
@@ -177,7 +177,7 @@ describe("getAppHref", () => {
path: "/path-base",
});
expect(href).toBe(
`/path-base/@${MockWorkspace.owner_name}/Test-Workspace.a-workspace-agent/apps/${app.slug}/`,
`/path-base/@${MockWorkspace.owner_name}/test-workspace.a-workspace-agent/apps/${app.slug}/`,
);
});
});
+4 -1
View File
@@ -1160,6 +1160,7 @@ const AgentChatPage: FC = () => {
isUpdateChatPlanModePending || isUpdateChatWorkspacePending;
const isInputDisabled =
!hasModelOptions || isArchived || isChatSettingsPending || isViewerNotOwner;
const canUpdateChatWorkspace = !isArchived && !isViewerNotOwner;
const selectedWorkspaceId = chatQuery.data?.workspace_id ?? null;
const isWorkspaceLoading =
@@ -1633,7 +1634,9 @@ const AgentChatPage: FC = () => {
isInterruptPending={isInterruptPending}
workspaceOptions={workspaceOptions}
selectedWorkspaceId={selectedWorkspaceId}
onWorkspaceChange={handleWorkspaceChange}
onWorkspaceChange={
canUpdateChatWorkspace ? handleWorkspaceChange : undefined
}
isWorkspaceLoading={isWorkspaceLoading}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
@@ -673,7 +673,22 @@ export const WorkspaceAgentStartTimeout: Story = {
};
export const WorkspaceNoAgent: Story = {
render: () => <StoryAgentChatPageView workspace={MockWorkspace} />,
render: () => (
<StoryAgentChatPageView
workspace={MockWorkspace}
workspaceOptions={[MockWorkspace]}
selectedWorkspaceId={MockWorkspace.id}
onWorkspaceChange={fn()}
/>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
canvas.getByRole("button", {
name: `Remove workspace ${MockWorkspace.name}`,
}),
).toBeVisible();
},
};
// ---------------------------------------------------------------------------
@@ -226,7 +226,7 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
isInterruptPending,
workspaceOptions = [],
selectedWorkspaceId = null,
onWorkspaceChange = () => {},
onWorkspaceChange,
isWorkspaceLoading = false,
isSidebarCollapsed,
onToggleSidebarCollapsed,
@@ -910,15 +910,16 @@ export const DetailPageWorkspacePicker: Story = {
statusLabel: "Workspace running",
},
},
play: async ({ canvasElement }) => {
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getAllByText("agents-workspace")).toHaveLength(1);
expect(
canvas.queryByRole("button", {
name: "Remove workspace agents-workspace",
}),
).not.toBeInTheDocument();
const removeWorkspaceButton = canvas.getByRole("button", {
name: "Remove workspace agents-workspace",
});
expect(removeWorkspaceButton).toBeVisible();
await userEvent.click(removeWorkspaceButton);
expect(args.onWorkspaceChange).toHaveBeenCalledWith(null);
const moreOptionsButton = canvas.getByRole("button", {
name: "More options",
@@ -940,6 +941,106 @@ export const DetailPageWorkspacePicker: Story = {
},
};
export const LinkedWorkspaceRemoveWhenInputDisabled: Story = {
args: {
isDisabled: true,
workspace: MockWorkspace,
workspaceAgent: MockWorkspaceAgent,
chatId: "chat-detail",
selectedWorkspaceId: MockWorkspace.id,
onWorkspaceChange: fn(),
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const workspaceMenuButton = canvas.getByRole("button", {
name: `${MockWorkspace.name} workspace menu`,
});
expect(
canvas.queryByRole("button", {
name: `Remove workspace ${MockWorkspace.name}`,
}),
).not.toBeInTheDocument();
expect(workspaceMenuButton).toBeVisible();
expect(workspaceMenuButton).toBeEnabled();
await userEvent.click(workspaceMenuButton);
let detachWorkspaceItem: HTMLElement | null = null;
await waitFor(() => {
const menuId = workspaceMenuButton.getAttribute("aria-controls");
if (!menuId) {
throw new Error("Expected workspace pill to control a menu.");
}
const menu = canvasElement.ownerDocument.getElementById(menuId);
if (!(menu instanceof HTMLElement)) {
throw new Error("Expected workspace menu to render.");
}
detachWorkspaceItem = within(menu).getByRole("menuitem", {
name: "Detach workspace",
});
expect(detachWorkspaceItem).toBeVisible();
});
if (!detachWorkspaceItem) {
throw new Error("Expected detach workspace menu item to render.");
}
await userEvent.click(detachWorkspaceItem);
expect(args.onWorkspaceChange).toHaveBeenCalledWith(null);
},
};
export const UncheckSelectedWorkspaceFromPicker: Story = {
args: {
isDisabled: true,
workspace: MockWorkspace,
workspaceAgent: MockWorkspaceAgent,
chatId: "chat-detail",
workspaceOptions: [
{
id: MockWorkspace.id,
name: MockWorkspace.name,
owner_name: MockWorkspace.owner_name,
organization_id: MockWorkspace.organization_id,
},
],
selectedWorkspaceId: MockWorkspace.id,
onWorkspaceChange: fn(),
},
parameters: {
viewport: { defaultViewport: "mobile1" },
chromatic: { viewports: [375] },
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
const moreOptionsButton = canvas.getByRole("button", {
name: "More options",
});
expect(moreOptionsButton).toBeEnabled();
await userEvent.click(moreOptionsButton);
const attachWorkspaceButton = (
await body.findByText("Attach workspace")
).closest("button");
if (!(attachWorkspaceButton instanceof HTMLButtonElement)) {
throw new Error("Expected Attach workspace to be a button.");
}
expect(attachWorkspaceButton).toBeEnabled();
await userEvent.click(attachWorkspaceButton);
const workspaceMatches = await body.findAllByText(MockWorkspace.name);
const selectedWorkspaceOption = workspaceMatches.at(-1);
if (!(selectedWorkspaceOption instanceof HTMLElement)) {
throw new Error("Expected workspace option to render.");
}
await userEvent.click(selectedWorkspaceOption);
expect(args.onWorkspaceChange).toHaveBeenCalledWith(null);
},
};
const confluenceMCP = makeMCPServer({
id: "mcp-confluence",
display_name: "Confluence Cloud",
@@ -211,10 +211,12 @@ const BadgeDismissButton: FC<{
type="button"
onClick={onClick}
disabled={isDisabled}
className="ml-0.5 inline-flex cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0.5 text-content-secondary transition-colors hover:bg-surface-tertiary hover:text-content-primary disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:text-content-secondary"
className="group -mx-1 -my-1 inline-flex size-5 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-content-secondary disabled:cursor-not-allowed disabled:opacity-50"
aria-label={ariaLabel}
>
<XIcon className="!size-2.5" />
<span className="inline-flex size-3.5 items-center justify-center rounded-full transition-colors group-hover:bg-surface-tertiary group-hover:text-content-primary">
<XIcon className="!size-2.5" />
</span>
</button>
);
@@ -233,18 +235,28 @@ const ToolBadge: FC<{
return (
<Tooltip>
<TooltipTrigger asChild>
<Link
to={badge.route}
target="_blank"
rel="noreferrer"
<span
className={cn(
badgeCls,
"no-underline transition-colors hover:bg-surface-tertiary hover:text-content-primary",
"transition-colors hover:bg-surface-tertiary hover:text-content-primary",
)}
>
{badge.statusIcon}
<span className="truncate">{badge.name}</span>
</Link>
<Link
to={badge.route}
target="_blank"
rel="noreferrer"
className="inline-flex min-w-0 items-center gap-1 text-inherit no-underline"
>
{badge.statusIcon}
<span className="truncate">{badge.name}</span>
</Link>
{onRemoveWorkspace && (
<BadgeDismissButton
onClick={onRemoveWorkspace}
ariaLabel={`Remove workspace ${badge.name}`}
/>
)}
</span>
</TooltipTrigger>
<TooltipContent>{badge.statusLabel}</TooltipContent>
</Tooltip>
@@ -491,10 +503,12 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
const selectedWorkspace = workspaceOptions?.find(
(ws) => ws.id === selectedWorkspaceId,
);
const canUseWorkspacePicker =
Boolean(onWorkspaceChange) && !isWorkspaceLoading;
const linkedWorkspaceId = workspace?.id ?? attachedWorkspace?.id;
const shouldShowSelectedWorkspaceBadge = selectedWorkspace
? Boolean(onWorkspaceChange) &&
selectedWorkspace.id !== attachedWorkspace?.id
? selectedWorkspace.id !== linkedWorkspaceId
: false;
const enabledMcpServers = mcpServers?.filter((s) => s.enabled) ?? [];
@@ -529,6 +543,9 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
const overflowBadges = allBadges.slice(visibleCount);
const handleRemoveWorkspace = () => onWorkspaceChange?.(null);
const removeWorkspaceHandler = onWorkspaceChange
? handleRemoveWorkspace
: undefined;
const handleRemoveMcp = (serverId: string) =>
handleMcpToggle(serverId, false);
@@ -1128,7 +1145,9 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
variant="subtle"
size="icon"
className="size-7 shrink-0 rounded-full [&>svg]:!size-icon-sm [&>svg]:p-0"
disabled={isDisabled && !agentSetupNotice}
disabled={
isDisabled && !agentSetupNotice && !canUseWorkspacePicker
}
aria-label="More options"
>
<PlusIcon />
@@ -1197,7 +1216,7 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
(isBelowMdViewport() ? (
<button
type="button"
disabled={isDisabled || isWorkspaceLoading}
disabled={!canUseWorkspacePicker}
onClick={() => setPlusMenuView("workspace")}
className="group flex h-8 w-full cursor-pointer items-center gap-1.5 border-none bg-transparent px-1 text-xs text-content-secondary shadow-none transition-colors hover:text-content-primary disabled:cursor-not-allowed disabled:opacity-50"
>
@@ -1213,7 +1232,7 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
<PopoverTrigger asChild>
<button
type="button"
disabled={isDisabled || isWorkspaceLoading}
disabled={!canUseWorkspacePicker}
className="group flex h-8 w-full cursor-pointer items-center gap-1.5 border-none bg-transparent px-1 text-xs text-content-secondary shadow-none transition-colors hover:text-content-primary disabled:cursor-not-allowed disabled:opacity-50"
>
<MonitorIcon className="size-3.5 shrink-0" />
@@ -1352,6 +1371,7 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
chatId={chatId}
sshCommand={sshCommand}
folder={folder}
onRemoveWorkspace={removeWorkspaceHandler}
/>
</span>
)}
@@ -1365,7 +1385,7 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
<ToolBadge
key={badge.kind === "mcp" ? badge.server.id : badge.kind}
badge={badge}
onRemoveWorkspace={handleRemoveWorkspace}
onRemoveWorkspace={removeWorkspaceHandler}
onRemoveMcp={handleRemoveMcp}
className={isOverflow ? "invisible order-1" : undefined}
/>
@@ -1405,7 +1425,7 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
: `${badge.kind}-overflow`
}
badge={badge}
onRemoveWorkspace={handleRemoveWorkspace}
onRemoveWorkspace={removeWorkspaceHandler}
onRemoveMcp={handleRemoveMcp}
/>
))}
@@ -1528,7 +1548,8 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
/**
* Shared workspace picker used by both the mobile and desktop
* "Attach workspace" menus. Workspaces from a different organization
* than the chat are rendered as disabled items with a tooltip.
* than the chat are disabled unless already selected, so stale bindings
* can still be cleared.
*/
interface WorkspacePickerListProps {
workspaceOptions:
@@ -1540,7 +1561,7 @@ interface WorkspacePickerListProps {
| undefined;
selectedWorkspaceId?: string | null;
chatOrganizationId?: string;
onSelect: (id: string) => void;
onSelect: (id: string | null) => void;
}
const WorkspacePickerList: FC<WorkspacePickerListProps> = ({
@@ -1559,31 +1580,33 @@ const WorkspacePickerList: FC<WorkspacePickerListProps> = ({
const isCrossOrg =
!!chatOrganizationId &&
workspace.organization_id !== chatOrganizationId;
const isSelected = selectedWorkspaceId === workspace.id;
const isUnavailable = isCrossOrg && !isSelected;
const item = (
<CommandItem
className={cn(
"text-xs font-normal",
isCrossOrg &&
isUnavailable &&
"cursor-not-allowed opacity-50 data-[disabled=true]:pointer-events-auto",
)}
key={workspace.id}
value={workspace.name}
disabled={isCrossOrg}
disabled={isUnavailable}
onSelect={() => {
if (!isCrossOrg) {
onSelect(workspace.id);
if (!isUnavailable) {
onSelect(isSelected ? null : workspace.id);
}
}}
>
{workspace.name}
{selectedWorkspaceId === workspace.id && (
{isSelected && (
<CheckIcon className="ml-auto size-icon-sm shrink-0" />
)}
</CommandItem>
);
if (isCrossOrg) {
if (isUnavailable) {
return (
<Tooltip key={workspace.id}>
<TooltipTrigger asChild>
@@ -206,7 +206,7 @@ interface ChatPageInputProps {
workspaceOptions: readonly TypesGen.Workspace[];
chatOrganizationId?: string;
selectedWorkspaceId: string | null;
onWorkspaceChange: (workspaceId: string | null) => void;
onWorkspaceChange?: (workspaceId: string | null) => void;
isWorkspaceLoading: boolean;
workspace?: TypesGen.Workspace;
workspaceAgent?: TypesGen.WorkspaceAgent;
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, userEvent, waitFor, within } from "storybook/test";
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
import type { WorkspaceApp } from "#/api/typesGenerated";
import {
MockListeningPortsResponse,
@@ -139,7 +139,7 @@ export const WithAllApps: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const pill = canvas.getByText("Test-Workspace");
const pill = canvas.getByText("test-workspace");
await userEvent.click(pill);
await waitFor(() => {
@@ -168,6 +168,51 @@ export const WithAllApps: Story = {
},
};
export const WithRemoveAction: Story = {
args: {
...defaultProps,
workspace: MockWorkspace,
agent: agentWithApps,
onRemoveWorkspace: fn(),
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const workspaceMenuButton = canvas.getByRole("button", {
name: `${MockWorkspace.name} workspace menu`,
});
expect(
canvas.queryByRole("button", {
name: `Remove workspace ${MockWorkspace.name}`,
}),
).not.toBeInTheDocument();
await userEvent.click(workspaceMenuButton);
let detachWorkspaceItem: HTMLElement | null = null;
await waitFor(() => {
const menuId = workspaceMenuButton.getAttribute("aria-controls");
if (!menuId) {
throw new Error("Expected workspace pill to control a menu.");
}
const menu = canvasElement.ownerDocument.getElementById(menuId);
if (!(menu instanceof HTMLElement)) {
throw new Error("Expected workspace menu to render.");
}
detachWorkspaceItem = within(menu).getByRole("menuitem", {
name: "Detach workspace",
});
expect(detachWorkspaceItem).toBeVisible();
});
if (!detachWorkspaceItem) {
throw new Error("Expected detach workspace menu item to render.");
}
await userEvent.click(detachWorkspaceItem);
expect(args.onRemoveWorkspace).toHaveBeenCalledTimes(1);
},
};
export const WithBuiltinAppsOnly: Story = {
args: {
...defaultProps,
@@ -176,7 +221,7 @@ export const WithBuiltinAppsOnly: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const pill = canvas.getByText("Test-Workspace");
const pill = canvas.getByText("test-workspace");
await userEvent.click(pill);
await waitFor(() => {
@@ -198,7 +243,7 @@ export const WithExternalAppsOnly: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const pill = canvas.getByText("Test-Workspace");
const pill = canvas.getByText("test-workspace");
await userEvent.click(pill);
await waitFor(() => {
@@ -221,7 +266,7 @@ export const NoApps: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const pill = canvas.getByText("Test-Workspace");
const pill = canvas.getByText("test-workspace");
await userEvent.click(pill);
await waitFor(() => {
@@ -239,7 +284,7 @@ export const WithHiddenApp: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const pill = canvas.getByText("Test-Workspace");
const pill = canvas.getByText("test-workspace");
await userEvent.click(pill);
await waitFor(() => {
@@ -324,7 +369,7 @@ export const WithListeningPorts: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const pill = canvas.getByText("Test-Workspace");
const pill = canvas.getByText("test-workspace");
await userEvent.click(pill);
const body = within(document.body);
@@ -374,7 +419,7 @@ export const WithSharedPorts: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const pill = canvas.getByText("Test-Workspace");
const pill = canvas.getByText("test-workspace");
await userEvent.click(pill);
const body = within(document.body);
@@ -418,7 +463,7 @@ export const EmptyPorts: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const pill = canvas.getByText("Test-Workspace");
const pill = canvas.getByText("test-workspace");
await userEvent.click(pill);
const body = within(document.body);
@@ -10,6 +10,7 @@ import {
NetworkIcon,
RadioIcon,
SquareTerminalIcon,
UnlinkIcon,
} from "lucide-react";
import type { FC } from "react";
import { useState } from "react";
@@ -67,6 +68,7 @@ interface WorkspacePillProps {
chatId: string;
sshCommand?: string;
folder?: string;
onRemoveWorkspace?: () => void;
}
export const WorkspacePill: FC<WorkspacePillProps> = ({
@@ -75,6 +77,7 @@ export const WorkspacePill: FC<WorkspacePillProps> = ({
chatId,
sshCommand,
folder,
onRemoveWorkspace,
}) => {
const [open, setOpen] = useState(false);
const [tooltipOpen, setTooltipOpen] = useState(false);
@@ -107,41 +110,42 @@ export const WorkspacePill: FC<WorkspacePillProps> = ({
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<Tooltip
open={tooltipOpen}
onOpenChange={(v) => setTooltipOpen(v && !open)}
>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={`${workspace.name} workspace menu`}
className={cn(
"inline-flex min-w-0 items-center gap-1 rounded-full bg-surface-secondary text-xs font-medium text-content-secondary overflow-hidden md:min-w-[2.75rem]",
"cursor-pointer border-0 transition-colors hover:bg-surface-tertiary hover:text-content-primary",
"size-7 justify-center p-0 md:size-auto md:max-w-[200px] md:justify-start md:px-2 md:py-0.5",
)}
>
<StatusIcon
type={effectiveType}
className="size-icon-sm shrink-0 md:size-3"
/>
<span className="hidden min-w-0 truncate md:inline">
{workspace.name}
</span>
<ChevronDownIcon
<span className="inline-flex min-w-0 items-center overflow-hidden rounded-full bg-surface-secondary text-xs font-medium text-content-secondary md:min-w-[2.75rem]">
<Tooltip
open={tooltipOpen}
onOpenChange={(v) => setTooltipOpen(v && !open)}
>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={`${workspace.name} workspace menu`}
className={cn(
"hidden size-3 shrink-0 opacity-60 transition-transform md:block",
open && "rotate-180",
"inline-flex min-w-0 cursor-pointer items-center justify-center gap-1 rounded-full border-0 bg-transparent p-0 text-xs font-medium text-content-secondary transition-colors hover:bg-surface-tertiary hover:text-content-primary",
"size-7 md:size-auto md:max-w-[200px] md:justify-start md:px-2 md:py-0.5",
)}
/>
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent className="hidden md:block">
{statusLabel}
</TooltipContent>
</Tooltip>
>
<StatusIcon
type={effectiveType}
className="size-icon-sm shrink-0 md:size-3"
/>
<span className="hidden min-w-0 truncate md:inline">
{workspace.name}
</span>
<ChevronDownIcon
className={cn(
"hidden size-3 shrink-0 opacity-60 transition-transform md:block",
open && "rotate-180",
)}
/>
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent className="hidden md:block">
{statusLabel}
</TooltipContent>
</Tooltip>
</span>
<DropdownMenuContent
side="top"
@@ -208,6 +212,18 @@ export const WorkspacePill: FC<WorkspacePillProps> = ({
View Workspace
</Link>
</DropdownMenuItem>
{onRemoveWorkspace && (
<>
<DropdownMenuSeparator className="my-1" />
<DropdownMenuItem
className="text-content-destructive focus:text-content-destructive"
onClick={onRemoveWorkspace}
>
<UnlinkIcon className="size-3.5" />
Detach workspace
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
@@ -63,7 +63,7 @@ test("add 3 hours to deadline", async () => {
await user.click(addButton);
await user.click(addButton);
await screen.findByText(
`Shutdown time for "Test-Workspace" updated successfully.`,
`Shutdown time for "test-workspace" updated successfully.`,
);
expect(await screen.findByText("Stop in 6 hours")).toBeInTheDocument();
@@ -91,7 +91,7 @@ test("remove 2 hours to deadline", async () => {
await user.click(subButton);
await user.click(subButton);
await screen.findByText(
`Shutdown time for "Test-Workspace" updated successfully.`,
`Shutdown time for "test-workspace" updated successfully.`,
);
expect(await screen.findByText("Stop in an hour")).toBeInTheDocument();
@@ -119,7 +119,7 @@ test("rollback to previous deadline on error", async () => {
await user.click(addButton);
await user.click(addButton);
await screen.findByText(
`Failed to update shutdown time for "Test-Workspace". Please try again.`,
`Failed to update shutdown time for "test-workspace". Please try again.`,
);
// In case of an error, the schedule message should remain unchanged
expect(screen.getByText(initialScheduleMessage)).toBeInTheDocument();
@@ -140,7 +140,7 @@ test("request is only sent once when clicking multiple times", async () => {
await user.click(addButton);
await user.click(addButton);
await screen.findByText(
`Shutdown time for "Test-Workspace" updated successfully.`,
`Shutdown time for "test-workspace" updated successfully.`,
);
expect(updateDeadlineSpy).toHaveBeenCalledTimes(1);
});
@@ -287,7 +287,7 @@ describe("WorkspaceSchedulePage", () => {
await user.click(submitButton);
const notification = await screen.findByText(
`Schedule for workspace "Test-Workspace" updated successfully.`,
`Schedule for workspace "test-workspace" updated successfully.`,
);
expect(notification).toBeInTheDocument();
@@ -320,7 +320,7 @@ describe("WorkspaceSchedulePage", () => {
await user.click(submitButton);
const notification = await screen.findByText(
`Schedule for workspace "Test-Workspace" updated successfully.`,
`Schedule for workspace "test-workspace" updated successfully.`,
);
expect(notification).toBeInTheDocument();
@@ -345,7 +345,7 @@ describe("WorkspaceSchedulePage", () => {
await user.click(submitButton);
const notification = await screen.findByText(
`Schedule for workspace "Test-Workspace" updated successfully.`,
`Schedule for workspace "test-workspace" updated successfully.`,
);
expect(notification).toBeInTheDocument();
+1 -1
View File
@@ -1558,7 +1558,7 @@ export const MockBuilds = [
export const MockWorkspace: TypesGen.Workspace = {
id: "test-workspace",
name: "Test-Workspace",
name: "test-workspace",
created_at: "",
updated_at: "",
template_id: MockTemplate.id,