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