Dev #11
@@ -63,13 +63,13 @@ function buildRetryResponse(outcome: IssueRetryNowOutcome) {
|
||||
};
|
||||
}
|
||||
|
||||
async function flushAll() {
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
async function waitForUi(assertion: () => void) {
|
||||
await vi.waitFor(async () => {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
assertion();
|
||||
});
|
||||
}
|
||||
|
||||
function renderWithProviders(ui: ReactNode) {
|
||||
@@ -174,11 +174,12 @@ describe("IssueScheduledRetryCard", () => {
|
||||
act(() => {
|
||||
button!.click();
|
||||
});
|
||||
await flushAll();
|
||||
expect(retryNowMock).toHaveBeenCalledWith("issue-1");
|
||||
const finalButton = getRetryNowButton();
|
||||
expect(finalButton!.textContent ?? "").toContain("Promoted");
|
||||
expect(finalButton!.disabled).toBe(true);
|
||||
await waitForUi(() => {
|
||||
expect(retryNowMock).toHaveBeenCalledWith("issue-1");
|
||||
const finalButton = getRetryNowButton();
|
||||
expect(finalButton!.textContent ?? "").toContain("Promoted");
|
||||
expect(finalButton!.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows already promoted state when backend reports duplicate click", async () => {
|
||||
@@ -189,9 +190,10 @@ describe("IssueScheduledRetryCard", () => {
|
||||
act(() => {
|
||||
getRetryNowButton()!.click();
|
||||
});
|
||||
await flushAll();
|
||||
expect(getRetryNowButton()!.textContent ?? "").toContain("Already promoted");
|
||||
expect(container.querySelector('[data-testid="issue-scheduled-retry-error-band"]')).toBeNull();
|
||||
await waitForUi(() => {
|
||||
expect(getRetryNowButton()!.textContent ?? "").toContain("Already promoted");
|
||||
expect(container.querySelector('[data-testid="issue-scheduled-retry-error-band"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders an inline error band on backend failure", async () => {
|
||||
@@ -202,11 +204,12 @@ describe("IssueScheduledRetryCard", () => {
|
||||
act(() => {
|
||||
getRetryNowButton()!.click();
|
||||
});
|
||||
await flushAll();
|
||||
const band = container.querySelector('[data-testid="issue-scheduled-retry-error-band"]');
|
||||
expect(band).not.toBeNull();
|
||||
expect((band?.textContent ?? "")).toContain("Server error");
|
||||
expect(getRetryNowButton()!.disabled).toBe(false);
|
||||
await waitForUi(() => {
|
||||
const band = container.querySelector('[data-testid="issue-scheduled-retry-error-band"]');
|
||||
expect(band).not.toBeNull();
|
||||
expect((band?.textContent ?? "")).toContain("Server error");
|
||||
expect(getRetryNowButton()!.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces gate-suppressed outcome via the inline error band", async () => {
|
||||
@@ -217,10 +220,11 @@ describe("IssueScheduledRetryCard", () => {
|
||||
act(() => {
|
||||
getRetryNowButton()!.click();
|
||||
});
|
||||
await flushAll();
|
||||
const band = container.querySelector('[data-testid="issue-scheduled-retry-error-band"]');
|
||||
expect(band).not.toBeNull();
|
||||
expect((band?.textContent ?? "")).toContain("Promotion suppressed");
|
||||
expect(getRetryNowButton()!.disabled).toBe(false);
|
||||
await waitForUi(() => {
|
||||
const band = container.querySelector('[data-testid="issue-scheduled-retry-error-band"]');
|
||||
expect(band).not.toBeNull();
|
||||
expect((band?.textContent ?? "")).toContain("Promotion suppressed");
|
||||
expect(getRetryNowButton()!.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +95,24 @@ async function flushReact() {
|
||||
describe("Sidebar", () => {
|
||||
let container: HTMLDivElement;
|
||||
|
||||
async function renderSidebar() {
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Sidebar />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
@@ -107,21 +125,23 @@ describe("Sidebar", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("does not flash the Workspaces link while experimental settings are loading", async () => {
|
||||
mockInstanceSettingsApi.getExperimental.mockImplementation(() => new Promise(() => {}));
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
it("links the top search icon to the search page without showing Search in Work nav", async () => {
|
||||
mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: false });
|
||||
const root = await renderSidebar();
|
||||
|
||||
const topSearchLink = container.querySelector('a[aria-label="Search"]');
|
||||
expect(topSearchLink?.getAttribute("href")).toBe("/search");
|
||||
const workLinks = [...container.querySelectorAll("nav a")].map((anchor) => anchor.textContent?.trim());
|
||||
expect(workLinks).not.toContain("Search");
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Sidebar />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
root.unmount();
|
||||
});
|
||||
await flushReact();
|
||||
});
|
||||
|
||||
it("does not flash the Workspaces link while experimental settings are loading", async () => {
|
||||
mockInstanceSettingsApi.getExperimental.mockImplementation(() => new Promise(() => {}));
|
||||
const root = await renderSidebar();
|
||||
|
||||
expect(container.textContent).not.toContain("Workspaces");
|
||||
|
||||
@@ -132,19 +152,7 @@ describe("Sidebar", () => {
|
||||
|
||||
it("shows the Workspaces link when isolated workspaces are enabled", async () => {
|
||||
mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: true });
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Sidebar />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
const root = await renderSidebar();
|
||||
|
||||
const link = [...container.querySelectorAll("a")].find((anchor) => anchor.textContent === "Workspaces");
|
||||
expect(link?.getAttribute("href")).toBe("/workspaces");
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { NavLink } from "@/lib/router";
|
||||
import { SidebarSection } from "./SidebarSection";
|
||||
import { SidebarNavItem } from "./SidebarNavItem";
|
||||
import { SidebarProjects } from "./SidebarProjects";
|
||||
@@ -45,10 +46,6 @@ export function Sidebar() {
|
||||
const liveRunCount = liveRuns?.length ?? 0;
|
||||
const showWorkspacesLink = experimentalSettings?.enableIsolatedWorkspaces === true;
|
||||
|
||||
function openSearch() {
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "k", metaKey: true }));
|
||||
}
|
||||
|
||||
const pluginContext = {
|
||||
companyId: selectedCompanyId,
|
||||
companyPrefix: selectedCompany?.issuePrefix ?? null,
|
||||
@@ -60,12 +57,16 @@ export function Sidebar() {
|
||||
<div className="flex items-center gap-1 px-3 h-12 shrink-0">
|
||||
<SidebarCompanyMenu />
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground shrink-0"
|
||||
onClick={openSearch}
|
||||
aria-label="Search"
|
||||
title="Search"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
<NavLink to="/search">
|
||||
<Search className="h-4 w-4" />
|
||||
</NavLink>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -99,7 +100,6 @@ export function Sidebar() {
|
||||
|
||||
<SidebarSection label="Work">
|
||||
<SidebarNavItem to="/issues" label="Issues" icon={CircleDot} />
|
||||
<SidebarNavItem to="/search" label="Search" icon={Search} />
|
||||
<SidebarNavItem to="/routines" label="Routines" icon={Repeat} />
|
||||
<SidebarNavItem to="/goals" label="Goals" icon={Target} />
|
||||
{showWorkspacesLink ? (
|
||||
|
||||
Reference in New Issue
Block a user