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();
});
});
40 changes: 40 additions & 0 deletions frontend/src/__tests__/componentTests/BrowseRightRail.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

vi.mock('@/hooks/useCartCount', () => ({ useCartCount: () => 3 }));

import BrowseRightRail from '@/components/ui/BrowsePage/BrowseRightRail';

describe('BrowseRightRail', () => {
it('shows the cart count badge', () => {
render(
<BrowseRightRail isOpen={false} mode="properties" onSelect={vi.fn()} />
);
expect(screen.getByText('3')).toBeInTheDocument();
});

it('calls onSelect with the clicked mode', async () => {
const onSelect = vi.fn();
const user = userEvent.setup();
render(
<BrowseRightRail isOpen={false} mode="properties" onSelect={onSelect} />
);
await user.click(screen.getByRole('button', { name: /layer cart/i }));
expect(onSelect).toHaveBeenCalledWith('cart');
await user.click(screen.getByRole('button', { name: /properties/i }));
expect(onSelect).toHaveBeenCalledWith('properties');
});

it('marks only the open mode as aria-pressed', () => {
render(<BrowseRightRail isOpen={true} mode="cart" onSelect={vi.fn()} />);
expect(screen.getByRole('button', { name: /layer cart/i })).toHaveAttribute(
'aria-pressed',
'true'
);
expect(screen.getByRole('button', { name: /properties/i })).toHaveAttribute(
'aria-pressed',
'false'
);
});
});
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();
});
});
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
@@ -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');
});
});
59 changes: 59 additions & 0 deletions frontend/src/__tests__/unitTests/viewsForDataLink.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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) => `${base}/${seg}`
}));

import { useViewsForDataLinkQuery } from '@/queries/viewQueries';

const fakeResponse = (status: number, body: unknown) =>
({
ok: 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('useViewsForDataLinkQuery', () => {
it('returns the views array on success', async () => {
sendFetchRequest.mockResolvedValue(
fakeResponse(200, { views: [{ short_key: 'v1', name: 'A' }] })
);
const { result } = renderHook(() => useViewsForDataLinkQuery('k1'), {
wrapper
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toHaveLength(1);
});

it('treats 404 as an empty list', async () => {
sendFetchRequest.mockResolvedValue(fakeResponse(404, {}));
const { result } = renderHook(() => useViewsForDataLinkQuery('k1'), {
wrapper
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual([]);
});

it('is disabled without a sharing key', () => {
const { result } = renderHook(() => useViewsForDataLinkQuery(undefined), {
wrapper
});
expect(result.current.fetchStatus).toBe('idle');
expect(sendFetchRequest).not.toHaveBeenCalled();
});
});
Loading
Loading