diff --git a/canvas/src/app/__tests__/orgs-page.test.tsx b/canvas/src/app/__tests__/orgs-page.test.tsx
index 0b4d1e94..2b1e0c77 100644
--- a/canvas/src/app/__tests__/orgs-page.test.tsx
+++ b/canvas/src/app/__tests__/orgs-page.test.tsx
@@ -15,7 +15,7 @@
* - Polling: provisioning orgs schedule a 5s refresh (fake timers)
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { render, screen, waitFor, cleanup } from "@testing-library/react";
+import { render, screen, cleanup } from "@testing-library/react";
// ── Hoisted mocks ────────────────────────────────────────────────────────────
// vi.mock factories are hoisted above imports; any captured references must
@@ -131,10 +131,9 @@ describe("/orgs — error state", () => {
Promise.reject(new Error("GET /cp/orgs: 500"))
);
render();
- // PR #1243 replaced waitFor polling with vi.advanceTimersByTimeAsync(50),
- // which fires the timer but does not guarantee React render flush completes
- // before the assertion runs. Restores waitFor for the error-state test.
- await waitFor(() => expect(screen.getByText(/Error:/)).toBeTruthy());
+ await vi.advanceTimersByTimeAsync(50);
+ await vi.runAllTimersAsync();
+ expect(screen.getByText(/Error:/)).toBeTruthy();
expect(screen.getByRole("button", { name: /retry/i })).toBeTruthy();
});
});
diff --git a/canvas/src/components/StatusDot.tsx b/canvas/src/components/StatusDot.tsx
index 9dcddb41..89d93288 100644
--- a/canvas/src/components/StatusDot.tsx
+++ b/canvas/src/components/StatusDot.tsx
@@ -14,6 +14,8 @@ export function StatusDot({
return (
);
}
diff --git a/canvas/src/components/Tooltip.tsx b/canvas/src/components/Tooltip.tsx
index dbda50d2..8afb8543 100644
--- a/canvas/src/components/Tooltip.tsx
+++ b/canvas/src/components/Tooltip.tsx
@@ -3,6 +3,11 @@
import { useState, useRef, useEffect, useCallback, type ReactNode } from "react";
import { createPortal } from "react-dom";
+let tooltipIdCounter = 0;
+function nextId() {
+ return ++tooltipIdCounter;
+}
+
interface Props {
text: string;
children: ReactNode;
@@ -13,6 +18,7 @@ export function Tooltip({ text, children }: Props) {
const [pos, setPos] = useState({ x: 0, y: 0 });
const timerRef = useRef>(undefined);
const triggerRef = useRef(null);
+ const tooltipId = useRef(`tooltip-${nextId()}`);
useEffect(() => () => clearTimeout(timerRef.current), []);
@@ -31,11 +37,34 @@ export function Tooltip({ text, children }: Props) {
setShow(false);
}, []);
+ // Show tooltip on keyboard focus (Tab navigation)
+ const onFocus = useCallback(() => {
+ clearTimeout(timerRef.current);
+ if (triggerRef.current) {
+ const rect = triggerRef.current.getBoundingClientRect();
+ setPos({ x: rect.left, y: rect.top });
+ }
+ setShow(true);
+ }, []);
+
+ const onBlur = useCallback(() => {
+ setShow(false);
+ }, []);
+
return (
-
+
{children}
{show && text && createPortal(
diff --git a/canvas/src/components/tabs/BudgetSection.tsx b/canvas/src/components/tabs/BudgetSection.tsx
index dff2d8a6..1f3941e6 100644
--- a/canvas/src/components/tabs/BudgetSection.tsx
+++ b/canvas/src/components/tabs/BudgetSection.tsx
@@ -107,7 +107,7 @@ export function BudgetSection({ workspaceId }: Props) {
const progressPct =
budget && budget.budget_limit != null && budget.budget_limit > 0
- ? Math.min(100, Math.round((budget.budget_used / budget.budget_limit) * 100))
+ ? Math.min(100, Math.round(((budget.budget_used ?? 0) / budget.budget_limit) * 100))
: 0;
// ── Render ────────────────────────────────────────────────────────────────