UNPKG

datajunction-ui

Version:
364 lines (320 loc) 11.4 kB
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(); }); });