datajunction-ui
Version:
DataJunction UI
364 lines (320 loc) • 11.4 kB
JSX
import React from 'react';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
import { screen, waitFor } from '@testing-library/react';
import { render } from '../../../../setupTests';
import fetchMock from 'mocks/fetchMock';
import { AddEditNodePage } from '../index.jsx';
import { mocks } from '../../../../mocks/mockNodes';
import DJClientContext from '../../../providers/djclient';
fetchMock.enableMocks();
export const initializeMockDJClient = () => {
return {
DataJunctionAPI: {
namespace: _ => {
return [
{
name: 'default.contractors',
display_name: 'Default: Contractors',
version: 'v1.0',
type: 'source',
status: 'valid',
mode: 'published',
updated_at: '2023-08-21T16:48:53.246914+00:00',
},
{
name: 'default.num_repair_orders',
display_name: 'Default: Num Repair Orders',
version: 'v1.0',
type: 'metric',
status: 'valid',
mode: 'published',
updated_at: '2023-08-21T16:48:56.841704+00:00',
},
];
},
metrics: vi
.fn()
.mockReturnValue([
'default.num_repair_orders',
'default.some_other_metric',
]),
getNodeForEditing: vi.fn(),
namespaces: () => {
return [
{
namespace: 'default',
num_nodes: 33,
},
{
namespace: 'default123',
num_nodes: 0,
},
];
},
createNode: vi.fn(),
patchNode: vi.fn(),
node: vi.fn().mockResolvedValue({ columns: [] }),
tagsNode: vi.fn(),
validateNode: vi
.fn()
.mockResolvedValue({ status: 200, json: { columns: [] } }),
listTags: vi.fn().mockReturnValue([]),
metric: vi.fn().mockReturnValue(mocks.mockMetricNode),
nodesWithType: vi
.fn()
.mockReturnValueOnce(['a'])
.mockReturnValueOnce(['b'])
.mockReturnValueOnce(['default.repair_orders']),
listMetricMetadata: vi.fn().mockReturnValue({
directions: ['higher_is_better', 'lower_is_better', 'neutral'],
units: [
{ name: 'dollar', label: 'Dollar' },
{ name: 'second', label: 'Second' },
],
}),
users: vi.fn().mockReturnValue([
{
id: 123,
username: 'test_user',
},
{
id: 1111,
username: 'dj',
},
]),
whoami: vi.fn().mockReturnValue({
id: 123,
username: 'test_user',
}),
},
};
};
export const testElement = djClient => {
return (
<DJClientContext.Provider value={djClient}>
<AddEditNodePage />
</DJClientContext.Provider>
);
};
export const renderCreateNode = element => {
return render(
<MemoryRouter initialEntries={['/create/dimension/default']}>
<Routes>
<Route path="create/:nodeType/:initialNamespace" element={element} />
</Routes>
</MemoryRouter>,
);
};
export const renderCreateMetric = element => {
return render(
<MemoryRouter initialEntries={['/create/metric/default']}>
<Routes>
<Route path="create/:nodeType/:initialNamespace" element={element} />
</Routes>
</MemoryRouter>,
);
};
export const renderEditNode = element => {
return render(
<MemoryRouter initialEntries={['/nodes/default.num_repair_orders/edit']}>
<Routes>
<Route path="nodes/:name/edit" element={element} />
</Routes>
</MemoryRouter>,
);
};
export const renderEditTransformNode = element => {
return render(
<MemoryRouter
initialEntries={['/nodes/default.repair_order_transform/edit']}
>
<Routes>
<Route path="nodes/:name/edit" element={element} />
</Routes>
</MemoryRouter>,
);
};
export const renderEditDerivedMetricNode = element => {
return render(
<MemoryRouter initialEntries={['/nodes/default.revenue_per_order/edit']}>
<Routes>
<Route path="nodes/:name/edit" element={element} />
</Routes>
</MemoryRouter>,
);
};
describe('AddEditNodePage', () => {
beforeEach(() => {
fetchMock.resetMocks();
vi.clearAllMocks();
window.scrollTo = vi.fn();
});
it('Create node page renders with the selected nodeType and namespace', async () => {
const mockDjClient = initializeMockDJClient();
const element = testElement(mockDjClient);
const { container } = renderCreateNode(element);
// The node type should be included in the page title
await waitFor(() => {
expect(
container.getElementsByClassName('node_type__metric'),
).toMatchSnapshot();
// The namespace should be set to the one provided in params
screen
.getAllByText('default')
.forEach(element => expect(element).toBeInTheDocument());
});
});
it('Edit node page renders with the selected node', async () => {
const mockDjClient = initializeMockDJClient();
mockDjClient.DataJunctionAPI.getNodeForEditing.mockReturnValue(
mocks.mockGetMetricNode,
);
const element = testElement(mockDjClient);
renderEditNode(element);
await waitFor(() => {
// Should be an edit node page
expect(screen.getByText('Edit')).toBeInTheDocument();
// The node name should be loaded onto the page
expect(screen.getByText('default.num_repair_orders')).toBeInTheDocument();
// The node type should be loaded onto the page
expect(screen.getByText('metric')).toBeInTheDocument();
// The description should be populated
expect(screen.getByText('Number of repair orders')).toBeInTheDocument();
// The upstream node should be populated
expect(screen.getByText('default.repair_orders')).toBeInTheDocument();
// The aggregate expression should be populated
expect(screen.getByText('count')).toBeInTheDocument();
expect(screen.getByText('(repair_order_id)')).toBeInTheDocument();
});
});
it('redirects the edit route to the node view for a read-only (git-backed) node', async () => {
const mockDjClient = initializeMockDJClient();
mockDjClient.DataJunctionAPI.namespaceSources = vi
.fn()
.mockResolvedValue(null);
mockDjClient.DataJunctionAPI.listDeployments = vi
.fn()
.mockResolvedValue([]);
// magnesium.tech is a flat git-backed namespace → read-only via shape.
mockDjClient.DataJunctionAPI.getNamespaceGitConfig = vi
.fn()
.mockResolvedValue({
github_repo_path: 'corp/tech-realtime-guardrails',
git_branch: 'main',
git_root_namespace: 'magnesium.tech',
});
const LocationDisplay = () => (
<div data-testid="loc">{useLocation().pathname}</div>
);
render(
<MemoryRouter
initialEntries={['/nodes/magnesium.tech.withinapp_ttr_log/edit']}
>
<LocationDisplay />
<Routes>
<Route path="nodes/:name/edit" element={testElement(mockDjClient)} />
<Route path="nodes/:name" element={<div>node view</div>} />
</Routes>
</MemoryRouter>,
);
// Redirected away from the edit route to the node view...
await waitFor(() =>
expect(screen.getByTestId('loc').textContent).toBe(
'/nodes/magnesium.tech.withinapp_ttr_log',
),
);
// ...and the editor form ("Edit" heading) never rendered.
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
}, 60000);
it('Verify edit page node not found', async () => {
const mockDjClient = initializeMockDJClient();
mockDjClient.DataJunctionAPI.getNodeForEditing.mockReturnValue(null);
const element = testElement(mockDjClient);
renderEditNode(element);
await waitFor(() => {
expect(mockDjClient.DataJunctionAPI.getNodeForEditing).toBeCalledTimes(1);
expect(
screen.getByText('Node default.num_repair_orders does not exist!'),
).toBeInTheDocument();
});
}, 60000);
it('Verify only transforms, metrics, and dimensions can be edited', async () => {
const mockDjClient = initializeMockDJClient();
mockDjClient.DataJunctionAPI.getNodeForEditing.mockReturnValue(
mocks.mockGetSourceNode,
);
const element = testElement(mockDjClient);
renderEditNode(element);
await waitFor(() => {
expect(mockDjClient.DataJunctionAPI.getNodeForEditing).toBeCalledTimes(1);
expect(
screen.getByText(
'Node default.num_repair_orders is of type source and cannot be edited',
),
).toBeInTheDocument();
});
}, 60000);
it('Edit page renders correctly for derived metric (metric parent)', async () => {
const mockDjClient = initializeMockDJClient();
mockDjClient.DataJunctionAPI.getNodeForEditing.mockReturnValue(
mocks.mockGetDerivedMetricNode,
);
const element = testElement(mockDjClient);
renderEditDerivedMetricNode(element);
await waitFor(() => {
// Should be an edit node page
expect(screen.getByText('Edit')).toBeInTheDocument();
// The node name should be loaded onto the page
expect(screen.getByText('default.revenue_per_order')).toBeInTheDocument();
// The node type should be loaded onto the page
expect(screen.getByText('metric')).toBeInTheDocument();
// The description should be populated
expect(
screen.getByText('Average revenue per order (derived metric)'),
).toBeInTheDocument();
// For derived metrics, the upstream node select should show the placeholder
// (indicating no upstream node is selected - derived metrics have metric parents)
expect(
screen.getByText('Select Upstream Node (optional for derived metrics)'),
).toBeInTheDocument();
});
});
it('Create metric page renders correctly', async () => {
const mockDjClient = initializeMockDJClient();
const element = testElement(mockDjClient);
renderCreateMetric(element);
await waitFor(() => {
// Should be a create metric page
expect(screen.getByText('Create')).toBeInTheDocument();
// The metric form should show the derived metric expression label
// (when no upstream is selected, we're in derived metric mode)
expect(
screen.getByText('Derived Metric Expression *'),
).toBeInTheDocument();
// The help text for derived metrics should be visible
expect(
screen.getByText(/Reference other metrics using their full names/),
).toBeInTheDocument();
});
});
it('Metric page handles error loading metrics gracefully', async () => {
const mockDjClient = initializeMockDJClient();
// Make metrics() throw an error
mockDjClient.DataJunctionAPI.metrics.mockRejectedValue(
new Error('Network error'),
);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const element = testElement(mockDjClient);
renderCreateMetric(element);
await waitFor(() => {
// The page should still render despite the error
expect(screen.getByText('Create')).toBeInTheDocument();
// The error should be logged
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to load metrics for autocomplete:',
expect.any(Error),
);
});
consoleSpy.mockRestore();
});
});