Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions frontend/src/__tests__/componentTests/AppearsInViews.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';

const { useViewsForDataLinkQuery } = vi.hoisted(() => ({
useViewsForDataLinkQuery: vi.fn()
}));
vi.mock('@/queries/viewQueries', () => ({ useViewsForDataLinkQuery }));

import AppearsInViews from '@/components/ui/PropertiesDrawer/AppearsInViews';

describe('AppearsInViews', () => {
it('lists the dependent Views with a count', () => {
useViewsForDataLinkQuery.mockReturnValue({
data: [
{ short_key: 'v1', name: 'Alpha' },
{ short_key: 'v2', name: 'Beta' }
],
isPending: false,
isError: false
});
render(<AppearsInViews sharingKey="k1" />);
expect(screen.getByText(/appears in 2 views/i)).toBeInTheDocument();
expect(screen.getByText('Alpha')).toBeInTheDocument();
expect(screen.getByText('Beta')).toBeInTheDocument();
});

it('renders nothing when there are no dependent Views', () => {
useViewsForDataLinkQuery.mockReturnValue({
data: [],
isPending: false,
isError: false
});
const { container } = render(<AppearsInViews sharingKey="k1" />);
expect(container).toBeEmptyDOMElement();
});
});
4 changes: 3 additions & 1 deletion frontend/src/__tests__/componentTests/Browse.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ vi.mock('react-router', async () => {
showPermissionsDialog: false,
showPropertiesDrawer: false,
showSidebar: false,
showConvertFileDialog: false
showConvertFileDialog: false,
propertiesDrawerMode: 'properties',
selectDrawerMode: vi.fn()
})
};
});
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/__tests__/componentTests/CartList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import type { CartItem } from '@/contexts/CartContext';

const cartA: CartItem = { fsp_name: 'f', path: '/a', label: 'Dataset A' };
const cartB: CartItem = { fsp_name: 'f', path: '/b', label: 'Dataset B' };

let cart: CartItem[] = [];
vi.mock('@/contexts/CartContext', () => ({
useCartContext: () => ({
cart,
clearCart: vi.fn().mockResolvedValue(undefined)
})
}));
vi.mock('@/queries/proxiedPathQueries', () => ({
useAllProxiedPathsQuery: () => ({ data: [] })
}));
vi.mock('@/components/ui/Views/CartDatasetRow', () => ({
default: ({ label }: { label: string }) => (
<div data-testid="row">{label}</div>
)
}));
vi.mock('@/components/ui/Views/CreateViewButton', () => ({
default: ({ label }: { label?: string }) => (
<button type="button">{label ?? 'Create View'}</button>
)
}));

import CartList from '@/components/ui/Views/CartList';

describe('CartList', () => {
it('shows the empty state when the cart is empty', () => {
cart = [];
render(<CartList />);
expect(screen.getByText(/your layer cart is empty/i)).toBeInTheDocument();
});

it('renders one row per dataset plus the footer actions', () => {
cart = [cartA, cartB];
render(<CartList />);
expect(screen.getAllByTestId('row')).toHaveLength(2);
expect(
screen.getByRole('button', { name: /create view/i })
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /clear cart/i })
).toBeInTheDocument();
});
});
5 changes: 2 additions & 3 deletions frontend/src/__tests__/componentTests/CartTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ vi.mock('@/components/ui/Views/CreateViewButton', () => ({
)
}));

import NGViews from '@/components/NGViews';
import CartList from '@/components/ui/Views/CartList';

beforeEach(() => {
addToCart.mockClear();
Expand All @@ -127,10 +127,9 @@ async function renderCartTab() {
const user = userEvent.setup();
render(
<MemoryRouter>
<NGViews />
<CartList />
</MemoryRouter>
);
await user.click(screen.getByRole('button', { name: /layer cart/i }));
return user;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// DataLinkDialog reads several contexts for the create branch; stub them so the
// delete branch renders standalone.
vi.mock('@/contexts/FileBrowserContext', () => ({
useFileBrowserContext: () => ({ fspName: 'f', filePath: '/a' })
}));
vi.mock('@/contexts/PreferencesContext', () => ({
usePreferencesContext: () => ({
pathPreference: ['linux_path'],
areDataLinksAutomatic: false,
dataLinkSubpathMode: 'name'
})
}));
vi.mock('@/contexts/ZonesAndFspMapContext', () => ({
useZoneAndFspMapContext: () => ({
zonesAndFspQuery: { isSuccess: false, data: {} }
})
}));

import DataLinkDialog from '@/components/ui/Dialogs/DataLink';
import { DependentViewsError } from '@/queries/proxiedPathQueries';
import type { ProxiedPath } from '@/contexts/ProxiedPathContext';

const proxiedPath = {
username: 'me',
sharing_key: 'k1',
sharing_name: 'n',
path: '/a',
fsp_name: 'f',
created_at: '',
updated_at: '',
url: 'http://x',
url_prefix: ''
} as ProxiedPath;

describe('DataLinkDialog delete → dependent Views', () => {
it('lists dependent Views on 409 and confirms with confirm=true', async () => {
const user = userEvent.setup();
const handleDeleteDataLink = vi
.fn()
.mockRejectedValueOnce(
new DependentViewsError('backs your Views', [
{ short_key: 'v1', name: 'My View' }
])
)
.mockResolvedValueOnce(undefined);

render(
<DataLinkDialog
action="delete"
handleDeleteDataLink={handleDeleteDataLink}
pending={false}
proxiedPath={proxiedPath}
setShowDataLinkDialog={vi.fn()}
showDataLinkDialog={true}
/>
);

await user.click(screen.getByRole('button', { name: /^delete$/i }));
expect(handleDeleteDataLink).toHaveBeenNthCalledWith(1, proxiedPath, false);
// confirm sub-view now lists the View
expect(await screen.findByText('My View')).toBeInTheDocument();

await user.click(screen.getByRole('button', { name: /delete anyway/i }));
expect(handleDeleteDataLink).toHaveBeenNthCalledWith(2, proxiedPath, true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ vi.mock('react-router', async () => {
showPermissionsDialog: false,
showPropertiesDrawer: false,
showSidebar: false,
showConvertFileDialog: false
showConvertFileDialog: false,
propertiesDrawerMode: 'properties',
selectDrawerMode: vi.fn()
})
};
});
Expand Down
21 changes: 2 additions & 19 deletions frontend/src/__tests__/componentTests/NGViews.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router';

import type { View } from '@/queries/viewQueries';
Expand Down Expand Up @@ -48,29 +47,13 @@ vi.mock('@/components/ui/Views/CreateViewButton', () => ({
import NGViews from '@/components/NGViews';

describe('NGViews page', () => {
it('shows Saved Views and Layer Cart tabs, with the seeded view listed', () => {
it('lists the saved views', () => {
render(
<MemoryRouter>
<NGViews />
</MemoryRouter>
);
expect(
screen.getByRole('button', { name: /saved views/i })
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /layer cart/i })
).toBeInTheDocument();
expect(screen.getByText('Neuroglancer Views')).toBeInTheDocument();
expect(screen.getByText('Seeded View')).toBeInTheDocument();
});

it('switches to the Layer Cart tab and shows the cart item', async () => {
const user = userEvent.setup();
render(
<MemoryRouter>
<NGViews />
</MemoryRouter>
);
await user.click(screen.getByRole('button', { name: /layer cart/i }));
expect(screen.getByText('a')).toBeInTheDocument(); // cart item label
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';

const sendFetchRequest = vi.fn();
vi.mock('@/utils', () => ({
sendFetchRequest: (...args: unknown[]) => sendFetchRequest(...args),
buildUrl: (
base: string,
seg: string | null,
q?: Record<string, string> | null
) => `${base}${seg ?? ''}${q ? '?' + new URLSearchParams(q).toString() : ''}`
}));

import {
useDeleteProxiedPathMutation,
DependentViewsError
} from '@/queries/proxiedPathQueries';

const fakeResponse = (status: number, body: unknown) =>
({
ok: status >= 200 && status < 300,
status,
statusText: String(status),
json: async () => body
}) as unknown as Response;

function wrapper({ children }: { children: ReactNode }) {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } }
});
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}

beforeEach(() => sendFetchRequest.mockReset());

describe('useDeleteProxiedPathMutation 409 handling', () => {
it('throws DependentViewsError carrying the dependent views on 409', async () => {
sendFetchRequest.mockResolvedValue(
fakeResponse(409, {
detail: {
message:
'This data link backs Views you own; they will be marked broken.',
dependent_views: [{ short_key: 'v1', name: 'My View' }]
}
})
);
const { result } = renderHook(() => useDeleteProxiedPathMutation(), {
wrapper
});
await expect(
result.current.mutateAsync({ sharing_key: 'k1' })
).rejects.toBeInstanceOf(DependentViewsError);
const err = (await result.current
.mutateAsync({ sharing_key: 'k1' })
.catch(e => e as DependentViewsError)) as DependentViewsError;
expect(err.views).toEqual([{ short_key: 'v1', name: 'My View' }]);
});

it('adds ?confirm=true when confirm is set and resolves on success', async () => {
sendFetchRequest.mockResolvedValue(
fakeResponse(200, { message: 'deleted' })
);
const { result } = renderHook(() => useDeleteProxiedPathMutation(), {
wrapper
});
await result.current.mutateAsync({ sharing_key: 'k1', confirm: true });
await waitFor(() =>
expect(sendFetchRequest).toHaveBeenCalledWith(
expect.stringContaining('confirm=true'),
'DELETE'
)
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';

vi.mock('@/contexts/PreferencesContext', () => ({
usePreferencesContext: () => ({
layout: '',
handleUpdateLayout: vi.fn().mockResolvedValue(undefined),
preferenceQuery: { isPending: false }
})
}));
vi.mock('@/contexts/ServerHealthContext', () => ({
useServerHealthContext: () => ({ status: 'up' })
}));

import useLayoutPrefs from '@/hooks/useLayoutPrefs';

describe('useLayoutPrefs drawer mode', () => {
beforeEach(() => {
// layout==='' on a wide screen opens the drawer in the init effect.
window.innerWidth = 1200;
});

it('defaults to properties mode', () => {
const { result } = renderHook(() => useLayoutPrefs());
expect(result.current.propertiesDrawerMode).toBe('properties');
});

it('selectDrawerMode opens the drawer and sets the mode', () => {
const { result } = renderHook(() => useLayoutPrefs());
act(() => result.current.selectDrawerMode('cart'));
expect(result.current.showPropertiesDrawer).toBe(true);
expect(result.current.propertiesDrawerMode).toBe('cart');
});

it('selecting the already-open mode closes the drawer', () => {
const { result } = renderHook(() => useLayoutPrefs());
act(() => result.current.selectDrawerMode('cart')); // open in cart
act(() => result.current.selectDrawerMode('cart')); // toggle closed
expect(result.current.showPropertiesDrawer).toBe(false);
});

it('switching mode while open keeps it open', () => {
const { result } = renderHook(() => useLayoutPrefs());
act(() => result.current.selectDrawerMode('cart'));
act(() => result.current.selectDrawerMode('properties'));
expect(result.current.showPropertiesDrawer).toBe(true);
expect(result.current.propertiesDrawerMode).toBe('properties');
});
});
Loading
Loading