feat: add network management page and inline accordion device details
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,678 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, waitFor, within, fireEvent, cleanup } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
// ── Shared mock state (vi.hoisted avoids TDZ with vi.mock hoisting) ────────────
|
||||
|
||||
const H = vi.hoisted(() => ({
|
||||
mockNavigate: vi.fn(),
|
||||
mockListNetworks: vi.fn(),
|
||||
mockListAvailableZtNetworks: vi.fn(),
|
||||
mockToast: vi.fn(),
|
||||
state: {
|
||||
orgId: "org-test-123" as string | null,
|
||||
},
|
||||
navigateCalls: [] as string[],
|
||||
}));
|
||||
|
||||
vi.mock("react-router-dom", async () => {
|
||||
const actual = await vi.importActual("react-router-dom");
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => {
|
||||
const fn = (path: string) => {
|
||||
H.navigateCalls.push(path);
|
||||
H.mockNavigate(path);
|
||||
};
|
||||
return fn;
|
||||
},
|
||||
useParams: () => ({ orgId: H.state.orgId }),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/hooks/useCurrentOrganization", () => ({
|
||||
useCurrentOrganizationId: () => ({
|
||||
orgId: H.state.orgId,
|
||||
isLoading: false,
|
||||
}),
|
||||
useCurrentOrganization: () => ({
|
||||
org: {
|
||||
id: H.state.orgId,
|
||||
name: "Test Org",
|
||||
slug: "test-org",
|
||||
description: null,
|
||||
logo_url: null,
|
||||
is_active: true,
|
||||
role: "admin",
|
||||
created_at: "2024-01-01",
|
||||
updated_at: "2024-01-01",
|
||||
},
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-toast", () => ({
|
||||
useToast: () => ({
|
||||
toast: H.mockToast,
|
||||
dismiss: () => {},
|
||||
toasts: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api", () => ({
|
||||
api: {
|
||||
zerotier: {
|
||||
listNetworks: H.mockListNetworks,
|
||||
listAvailableZtNetworks: H.mockListAvailableZtNetworks,
|
||||
createNetwork: vi.fn(),
|
||||
updateNetwork: vi.fn(),
|
||||
deleteNetwork: vi.fn(),
|
||||
},
|
||||
},
|
||||
ApiError: class ApiError extends Error {
|
||||
code: number;
|
||||
type: string;
|
||||
details: Record<string, unknown>;
|
||||
constructor(message: string, code: number, type: string, details: Record<string, unknown> = {}) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.code = code;
|
||||
this.type = type;
|
||||
this.details = details;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
import NetworksPage from "../src/pages/org/NetworksPage";
|
||||
|
||||
// ── Test data ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const MOCK_NETWORKS = [
|
||||
{
|
||||
id: "net-001",
|
||||
organization_id: "org-test-123",
|
||||
name: "Production VPN",
|
||||
description: "Main production network",
|
||||
owner_user_id: "user-1",
|
||||
zerotier_network_id: "d6578dd03c894448",
|
||||
environment: "production" as const,
|
||||
request_mode: "approval_required" as const,
|
||||
default_activation_lifetime_minutes: 480,
|
||||
max_activation_lifetime_minutes: null,
|
||||
is_active: true,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
deleted_at: null,
|
||||
approved_user_count: 25,
|
||||
active_membership_count: 12,
|
||||
},
|
||||
{
|
||||
id: "net-002",
|
||||
organization_id: "org-test-123",
|
||||
name: "Dev Network",
|
||||
description: "Development and staging",
|
||||
owner_user_id: "user-1",
|
||||
zerotier_network_id: "abcdef1234567890",
|
||||
environment: "development" as const,
|
||||
request_mode: "open" as const,
|
||||
default_activation_lifetime_minutes: 240,
|
||||
max_activation_lifetime_minutes: 1440,
|
||||
is_active: false,
|
||||
created_at: "2024-01-02T00:00:00Z",
|
||||
updated_at: "2024-01-02T00:00:00Z",
|
||||
deleted_at: null,
|
||||
approved_user_count: 5,
|
||||
active_membership_count: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_ZT_NETWORKS = [
|
||||
{
|
||||
id: "zt-net-001",
|
||||
name: "External ZeroTier",
|
||||
description: "An external ZT network",
|
||||
owner_id: null,
|
||||
online_member_count: 3,
|
||||
authorized_member_count: 10,
|
||||
total_member_count: 10,
|
||||
already_managed: false,
|
||||
portal_network_id: null,
|
||||
portal_network_name: null,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function renderPage() {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<NetworksPage />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
// Default: all API calls return never-resolving promise (loading state)
|
||||
// Individual tests override BEFORE calling renderPage().
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
H.navigateCalls.length = 0;
|
||||
H.mockListNetworks.mockImplementation(() => new Promise(() => {}));
|
||||
H.mockListAvailableZtNetworks.mockImplementation(() => new Promise(() => {}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HAPPY PATH: Data Loading
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("NetworksPage — Data Loading", () => {
|
||||
test("renders loading state while fetching networks", () => {
|
||||
renderPage();
|
||||
|
||||
expect(screen.getByText("Loading networks…")).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders network data when API resolves", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Dev Network")).toBeDefined();
|
||||
expect(screen.getByText("d6578dd03c894448")).toBeDefined();
|
||||
expect(screen.getByText("abcdef1234567890")).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders error state when API fails", async () => {
|
||||
H.mockListNetworks.mockRejectedValue(new Error("Network error"));
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load networks. Please try again."),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders empty state when no networks exist", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({ networks: [], count: 0 });
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("No networks configured yet. Add one to get started."),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// NAVIGATION: Row Click
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("NetworksPage — Row Click Navigation", () => {
|
||||
test("clicking a network row navigates to /org/zerotier/networks/{networkId}", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
const productionRow = screen.getByText("Production VPN").closest("button");
|
||||
expect(productionRow).not.toBeNull();
|
||||
|
||||
fireEvent.click(productionRow!);
|
||||
|
||||
expect(H.mockNavigate).toHaveBeenCalledTimes(1);
|
||||
expect(H.mockNavigate).toHaveBeenCalledWith("/org/zerotier/networks/net-001");
|
||||
expect(H.navigateCalls).toEqual(["/org/zerotier/networks/net-001"]);
|
||||
});
|
||||
|
||||
test("clicking second network row navigates to its URL", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Dev Network")).toBeDefined();
|
||||
});
|
||||
|
||||
const devRow = screen.getByText("Dev Network").closest("button");
|
||||
expect(devRow).not.toBeNull();
|
||||
|
||||
fireEvent.click(devRow!);
|
||||
|
||||
expect(H.mockNavigate).toHaveBeenCalledTimes(1);
|
||||
expect(H.mockNavigate).toHaveBeenCalledWith("/org/zerotier/networks/net-002");
|
||||
});
|
||||
|
||||
test("navigate NOT called before any click", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(H.mockNavigate).not.toHaveBeenCalled();
|
||||
expect(H.navigateCalls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// NAVIGATION: Dropdown "View details"
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("NetworksPage — Dropdown View details Navigation", () => {
|
||||
test('"View details" dropdown item navigates to /org/zerotier/networks/{networkId}', async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
// The MoreHorizontal button is a CHILD of the row button (nested inside it).
|
||||
// Find it by looking for the button with the MoreHorizontal icon inside the row.
|
||||
const productionRow = screen.getByText("Production VPN").closest("button")!;
|
||||
// Find ALL nested buttons within the row
|
||||
const nestedButtons = productionRow.querySelectorAll("button");
|
||||
// The first nested button should be the MoreHorizontal dropdown trigger
|
||||
expect(nestedButtons.length).toBeGreaterThan(0);
|
||||
// Radix DropdownMenu opens on pointerdown
|
||||
fireEvent.pointerDown(nestedButtons[0]);
|
||||
|
||||
// DropdownMenuContent renders in a portal, screen.getByText searches the whole document
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("View details")).toBeDefined();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("View details"));
|
||||
|
||||
expect(H.mockNavigate).toHaveBeenCalledWith("/org/zerotier/networks/net-001");
|
||||
});
|
||||
|
||||
test('"View details" for second network navigates to its URL', async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Dev Network")).toBeDefined();
|
||||
});
|
||||
|
||||
const devRow = screen.getByText("Dev Network").closest("button")!;
|
||||
const nestedButtons = devRow.querySelectorAll("button");
|
||||
expect(nestedButtons.length).toBeGreaterThan(0);
|
||||
fireEvent.pointerDown(nestedButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("View details")).toBeDefined();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("View details"));
|
||||
|
||||
expect(H.mockNavigate).toHaveBeenCalledWith("/org/zerotier/networks/net-002");
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CARD DESCRIPTION TEXT
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("NetworksPage — Card Description", () => {
|
||||
test("CardDescription reflects page navigation (not old drawer text)", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
const description = screen.getByText(
|
||||
"Click a network to manage members, devices, and access requests",
|
||||
);
|
||||
expect(description).toBeDefined();
|
||||
});
|
||||
|
||||
test("old drawer-related text is absent", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
// The old Sheet content (Network Details, member list) should NOT be present
|
||||
expect(screen.queryByText("Network Details")).toBeNull();
|
||||
expect(screen.queryByText("Members")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// ZERO TIER NETWORK PICKER SHEET
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("NetworksPage — ZeroTier Picker Sheet", () => {
|
||||
test('"Import from ZeroTier" button is present', async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
const importButton = screen.getByRole("button", {
|
||||
name: /import from zerotier/i,
|
||||
});
|
||||
expect(importButton).toBeDefined();
|
||||
});
|
||||
|
||||
test('clicking "Import from ZeroTier" opens the ZT Picker Sheet', async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
H.mockListAvailableZtNetworks.mockResolvedValue({
|
||||
networks: MOCK_ZT_NETWORKS,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
const importButton = screen.getByRole("button", {
|
||||
name: /import from zerotier/i,
|
||||
});
|
||||
fireEvent.click(importButton);
|
||||
|
||||
// Wait for the Sheet to render with its content
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("External ZeroTier")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.getByText("zt-net-001")).toBeDefined();
|
||||
expect(screen.getByText("Import")).toBeDefined();
|
||||
});
|
||||
|
||||
test("ZT Picker calls API with correct orgId", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
H.mockListAvailableZtNetworks.mockResolvedValue({
|
||||
networks: MOCK_ZT_NETWORKS,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
const importButton = screen.getByRole("button", {
|
||||
name: /import from zerotier/i,
|
||||
});
|
||||
fireEvent.click(importButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("External ZeroTier")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(H.mockListAvailableZtNetworks).toHaveBeenCalledTimes(1);
|
||||
expect(H.mockListAvailableZtNetworks).toHaveBeenCalledWith("org-test-123");
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// DATA DISPLAY
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("NetworksPage — Data Display", () => {
|
||||
test("displays network count badge", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
// The CardTitle contains "Portal Networks" and the count badge
|
||||
expect(screen.getByText("Portal Networks")).toBeDefined();
|
||||
// Badge with count "2" should be present
|
||||
expect(screen.getByText("2")).toBeDefined();
|
||||
});
|
||||
|
||||
test("displays approved user counts", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.getByText("25")).toBeDefined();
|
||||
expect(screen.getByText("5")).toBeDefined();
|
||||
});
|
||||
|
||||
test("displays active device counts", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.getByText("12")).toBeDefined();
|
||||
expect(screen.getByText("0")).toBeDefined();
|
||||
});
|
||||
|
||||
test("displays environment badges", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Production")).toBeDefined();
|
||||
expect(screen.getByText("Development")).toBeDefined();
|
||||
});
|
||||
|
||||
test("displays request mode badges", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Approval Required")).toBeDefined();
|
||||
expect(screen.getByText("Open")).toBeDefined();
|
||||
});
|
||||
|
||||
test('displays "Inactive" badge for inactive networks', async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Dev Network")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Inactive")).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders search-empty state when filter matches nothing", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: MOCK_NETWORKS,
|
||||
count: MOCK_NETWORKS.length,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search networks…");
|
||||
fireEvent.change(searchInput, { target: { value: "zzzz_nonexistent" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("No networks match your search."),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// EDGE CASES / ADVERSARIAL INPUTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("NetworksPage — Adversarial Inputs", () => {
|
||||
test("handles XSS-like network name as text", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: [
|
||||
{
|
||||
...MOCK_NETWORKS[0],
|
||||
name: 'VPN <script>alert("xss")</script>',
|
||||
description: "desc ${injection}",
|
||||
zerotier_network_id: "../../etc/passwd",
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('VPN <script>alert("xss")</script>'),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles very long network name", async () => {
|
||||
const longName = "A".repeat(500);
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: [{ ...MOCK_NETWORKS[0], name: longName, id: "net-long" }],
|
||||
count: 1,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(longName)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles Unicode network name", async () => {
|
||||
const unicodeName = "ネットワーク \u{1F525} 测试";
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: [
|
||||
{ ...MOCK_NETWORKS[0], name: unicodeName, id: "net-unicode" },
|
||||
],
|
||||
count: 1,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(unicodeName)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles missing optional counts (undefined)", async () => {
|
||||
H.mockListNetworks.mockResolvedValue({
|
||||
networks: [
|
||||
{
|
||||
...MOCK_NETWORKS[0],
|
||||
approved_user_count: undefined,
|
||||
active_membership_count: undefined,
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
});
|
||||
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Production VPN")).toBeDefined();
|
||||
});
|
||||
|
||||
// Should show "0" for undefined counts (nullish coalescing: ?? 0)
|
||||
const zeros = screen.getAllByText("0");
|
||||
expect(zeros.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user