UNPKG

datajunction-ui

Version:

DataJunction Metrics Platform UI

860 lines (798 loc) 27.6 kB
import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { mocks } from '../../../../mocks/mockNodes'; import DJClientContext from '../../../providers/djclient'; import { NodePage } from '../Loadable'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import userEvent from '@testing-library/user-event'; describe('<NodePage />', () => { const domTestingLib = require('@testing-library/dom'); const { queryHelpers } = domTestingLib; const queryByAttribute = attribute => queryHelpers.queryAllByAttribute.bind(null, attribute); function getByAttribute(container, id, attribute, ...rest) { const result = queryByAttribute(attribute)(container, id, ...rest); return result[0]; } const mockDJClient = () => { return { DataJunctionAPI: { node: jest.fn(), metric: jest.fn(), getMetric: jest.fn(), revalidate: jest.fn().mockReturnValue({ status: 'valid' }), node_dag: jest.fn().mockReturnValue(mocks.mockNodeDAG), clientCode: jest.fn().mockReturnValue('dj_client = DJClient()'), columns: jest.fn(), history: jest.fn(), revisions: jest.fn(), materializations: jest.fn(), materializationInfo: jest.fn(), sql: jest.fn(), cube: jest.fn(), compiledSql: jest.fn(), node_lineage: jest.fn(), nodesWithDimension: jest.fn(), attributes: jest.fn(), dimensions: jest.fn(), setPartition: jest.fn(), engines: jest.fn(), streamNodeData: jest.fn(), nodeDimensions: jest.fn(), }, }; }; const defaultProps = { name: 'default.avg_repair_price', djNode: { namespace: 'default', node_revision_id: 24, node_id: 24, type: 'metric', name: 'default.avg_repair_price', display_name: 'Default: Avg Repair Price', version: 'v1.0', status: 'valid', mode: 'published', catalog: { id: 1, uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', created_at: '2023-08-21T16:48:51.146121+00:00', updated_at: '2023-08-21T16:48:51.146122+00:00', extra_params: {}, name: 'warehouse', }, schema_: null, table: null, description: 'Average repair price', query: 'SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n', availability: null, dimension_links: [], columns: [ { name: 'default_DOT_avg_repair_price', type: 'double', display_name: 'Default DOT avg repair price', attributes: [], dimension: null, }, ], updated_at: '2023-08-21T16:48:56.932231+00:00', materializations: [], parents: [ { name: 'default.repair_order_details', }, ], created_at: '2023-08-21T16:48:56.932162+00:00', tags: [{ name: 'purpose', display_name: 'Purpose' }], primary_key: [], incompatible_druid_functions: ['IF'], createNodeClientCode: 'dj = DJBuilder(DJ_URL)\n\navg_repair_price = dj.create_metric(\n description="Average repair price",\n display_name="Default: Avg Repair Price",\n name="default.avg_repair_price",\n primary_key=[],\n query="""SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n"""\n)', dimensions: [ { name: 'default.date_dim.dateint', type: 'timestamp', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', 'default.hard_hat.birth_date', ], }, { name: 'default.date_dim.dateint', type: 'timestamp', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', 'default.hard_hat.hire_date', ], }, { name: 'default.date_dim.day', type: 'int', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', 'default.hard_hat.birth_date', ], }, { name: 'default.date_dim.day', type: 'int', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', 'default.hard_hat.hire_date', ], }, { name: 'default.date_dim.month', type: 'int', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', 'default.hard_hat.birth_date', ], }, { name: 'default.date_dim.month', type: 'int', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', 'default.hard_hat.hire_date', ], }, { name: 'default.date_dim.year', type: 'int', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', 'default.hard_hat.birth_date', ], }, { name: 'default.date_dim.year', type: 'int', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', 'default.hard_hat.hire_date', ], }, { name: 'default.hard_hat.address', type: 'string', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.birth_date', type: 'date', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.city', type: 'string', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.contractor_id', type: 'int', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.country', type: 'string', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.first_name', type: 'string', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.hard_hat_id', type: 'int', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.hire_date', type: 'date', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.last_name', type: 'string', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.manager', type: 'int', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.postal_code', type: 'string', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.state', type: 'string', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, { name: 'default.hard_hat.title', type: 'string', path: [ 'default.repair_order_details.repair_order_id', 'default.repair_order.hard_hat_id', ], }, ], }, }; it('renders the NodeInfo tab correctly for a metric node', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); djClient.DataJunctionAPI.getMetric.mockReturnValue( mocks.mockMetricNodeJson, ); const element = ( <DJClientContext.Provider value={djClient}> <NodePage {...defaultProps} /> </DJClientContext.Provider> ); const { container } = render( <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/info']}> <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); await waitFor(() => { expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith( 'default.num_repair_orders', ); expect( screen.getByRole('dialog', { name: 'NodeName' }), ).toHaveTextContent('default.num_repair_orders'); expect(screen.getByRole('button', { name: 'Info' })).toBeInTheDocument(); expect( screen.getByRole('dialog', { name: 'Description' }), ).toHaveTextContent('Number of repair orders'); expect(screen.getByRole('dialog', { name: 'Version' })).toHaveTextContent( 'v1.0', ); expect( screen.getByRole('dialog', { name: 'NodeStatus' }), ).toBeInTheDocument(); expect(screen.getByRole('dialog', { name: 'Tags' })).toHaveTextContent( 'Purpose', ); expect( screen.getByRole('dialog', { name: 'RequiredDimensions' }), ).toHaveTextContent(''); expect( screen.getByRole('dialog', { name: 'DisplayName' }), ).toHaveTextContent('Default: Num Repair Orders'); expect( screen.getByRole('dialog', { name: 'NodeType' }), ).toHaveTextContent('metric'); expect( container.getElementsByClassName('language-sql'), ).toMatchSnapshot(); }); }, 60000); it('renders the NodeInfo tab correctly for cube nodes', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockCubeNode); djClient.DataJunctionAPI.cube.mockReturnValue(mocks.mockCubesCube); const element = ( <DJClientContext.Provider value={djClient}> <NodePage {...defaultProps} /> </DJClientContext.Provider> ); const { container } = render( <MemoryRouter initialEntries={['/nodes/default.repair_orders_cube']}> <Routes> <Route path="nodes/:name" element={element} /> </Routes> </MemoryRouter>, ); await waitFor(() => { expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith( 'default.repair_orders_cube', ); userEvent.click(screen.getByRole('button', { name: 'Info' })); expect( screen.getByRole('dialog', { name: 'NodeName' }), ).toHaveTextContent('default.repair_orders_cube'); expect(screen.getByRole('button', { name: 'Info' })).toBeInTheDocument(); expect( screen.getByRole('dialog', { name: 'Description' }), ).toHaveTextContent('Repair Orders'); expect(screen.getByRole('dialog', { name: 'Version' })).toHaveTextContent( 'v1.0', ); expect( screen.getByRole('dialog', { name: 'PrimaryKey' }), ).toHaveTextContent(''); expect( screen.getByRole('dialog', { name: 'DisplayName' }), ).toHaveTextContent('Default: Repair Orders Cube'); expect( screen.getByRole('dialog', { name: 'NodeType' }), ).toHaveTextContent('cube'); expect( screen.getByRole('dialog', { name: 'NodeType' }), ).toHaveTextContent('cube'); expect(screen.getByText('Cube Elements')).toBeInTheDocument(); screen .getAllByRole('cell', { name: 'CubeElement' }) .map(cube => cube.hasAttribute('a')); }); }, 60000); it('renders the NodeColumns tab correctly', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); djClient.DataJunctionAPI.getMetric.mockReturnValue( mocks.mockMetricNodeJson, ); djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); djClient.DataJunctionAPI.attributes.mockReturnValue(mocks.attributes); djClient.DataJunctionAPI.dimensions.mockReturnValue(mocks.dimensions); djClient.DataJunctionAPI.engines.mockReturnValue([]); djClient.DataJunctionAPI.setPartition.mockReturnValue({ status: 200, json: { message: '' }, }); const element = ( <DJClientContext.Provider value={djClient}> <NodePage /> </DJClientContext.Provider> ); render( <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/columns']} > <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); await waitFor(() => { expect(djClient.DataJunctionAPI.columns).toHaveBeenCalledWith( mocks.mockMetricNode, ); expect( screen.getByRole('columnheader', { name: 'ColumnName' }), ).toHaveTextContent('default_DOT_avg_repair_price'); expect( screen.getByRole('columnheader', { name: 'ColumnDisplayName' }), ).toHaveTextContent('Default DOT avg repair price'); expect( screen.getByRole('columnheader', { name: 'ColumnType' }), ).toHaveTextContent('double'); // check that the edit column popover can be clicked const editColumnPopover = screen.getByRole('button', { name: 'EditColumn', }); expect(editColumnPopover).toBeInTheDocument(); fireEvent.click(editColumnPopover); expect( screen.getByRole('button', { name: 'SaveEditColumn' }), ).toBeInTheDocument(); // check that the link dimension popover can be clicked const linkDimensionPopover = screen.getByRole('button', { name: 'LinkDimension', }); expect(linkDimensionPopover).toBeInTheDocument(); fireEvent.click(linkDimensionPopover); expect( screen.getByRole('button', { name: 'SaveLinkDimension' }), ).toBeInTheDocument(); // check that the set column partition popover can be clicked const partitionColumnPopover = screen.getByRole('button', { name: 'PartitionColumn', }); expect(partitionColumnPopover).toBeInTheDocument(); fireEvent.click(partitionColumnPopover); const savePartition = screen.getByRole('button', { name: 'SaveEditColumn', }); expect(savePartition).toBeInTheDocument(); fireEvent.click(savePartition); expect(screen.getByText('Saved!')); }); }, 60000); // check compiled SQL on nodeInfo page it('renders the NodeHistory tab correctly', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); djClient.DataJunctionAPI.getMetric.mockReturnValue( mocks.mockMetricNodeJson, ); djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); djClient.DataJunctionAPI.history.mockReturnValue(mocks.metricNodeHistory); djClient.DataJunctionAPI.revisions.mockReturnValue( mocks.metricNodeRevisions, ); const element = ( <DJClientContext.Provider value={djClient}> <NodePage /> </DJClientContext.Provider> ); const { container } = render( <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/history']} > <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); await waitFor(async () => { fireEvent.click(screen.getByRole('button', { name: 'History' })); expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith( mocks.mockMetricNode.name, ); expect(djClient.DataJunctionAPI.history).toHaveBeenCalledWith( 'node', mocks.mockMetricNode.name, ); expect( screen.getByRole('list', { name: 'Activity' }), ).toBeInTheDocument(); screen .queryAllByRole('cell', { name: 'HistoryAttribute', }) .forEach(cell => expect(cell).toHaveTextContent(/Set col1 as /)); screen .queryAllByRole('cell', { name: 'HistoryCreateLink', }) .forEach(cell => expect(cell).toHaveTextContent( 'Linked col1 todefault.hard_hat viahard_hat_id', ), ); screen .queryAllByRole('cell', { name: 'HistoryCreateMaterialization', }) .forEach(cell => expect(cell).toHaveTextContent( 'Initialized materialization some_random_materialization', ), ); screen .queryAllByRole('cell', { name: 'HistoryNodeStatusChange', }) .forEach(cell => expect(cell).toHaveTextContent( 'Status changed from valid to invalid Caused by a change in upstream default.repair_order_details', ), ); }); }); it('renders compiled sql correctly', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockTransformNode); djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); djClient.DataJunctionAPI.compiledSql.mockReturnValue('select 1'); const element = ( <DJClientContext.Provider value={djClient}> <NodePage /> </DJClientContext.Provider> ); render( <MemoryRouter initialEntries={[`/nodes/${mocks.mockTransformNode.name}`]}> <Routes> <Route path="nodes/:name" element={element} /> </Routes> </MemoryRouter>, ); await waitFor(() => { fireEvent.click(screen.getByRole('checkbox', { name: 'ToggleSwitch' })); expect(djClient.DataJunctionAPI.compiledSql).toHaveBeenCalledWith( mocks.mockTransformNode.name, ); }); }); it('renders an empty NodeMaterialization tab correctly', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); djClient.DataJunctionAPI.getMetric.mockReturnValue( mocks.mockMetricNodeJson, ); djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); djClient.DataJunctionAPI.materializations.mockReturnValue([]); const element = ( <DJClientContext.Provider value={djClient}> <NodePage /> </DJClientContext.Provider> ); render( <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/materializations']} > <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); await waitFor(() => { fireEvent.click(screen.getByRole('button', { name: 'Materializations' })); expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith( mocks.mockMetricNode.name, ); screen.getByText( 'No materialization workflows configured for this node.', ); screen.getByText('No materialized datasets available for this node.'); }); }); it('renders the NodeMaterialization tab with materializations correctly', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockTransformNode); djClient.DataJunctionAPI.getMetric.mockReturnValue( mocks.mockMetricNodeJson, ); djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); djClient.DataJunctionAPI.materializations.mockReturnValue( mocks.nodeMaterializations, ); djClient.DataJunctionAPI.materializationInfo.mockReturnValue( mocks.materializationInfo, ); const element = ( <DJClientContext.Provider value={djClient}> <NodePage /> </DJClientContext.Provider> ); render( <MemoryRouter initialEntries={[ '/nodes/default.repair_order_transform/materializations', ]} > <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); await waitFor( () => { fireEvent.click( screen.getByRole('button', { name: 'Materializations' }), ); expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith( mocks.mockTransformNode.name, ); expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith( mocks.mockTransformNode.name, ); }, { timeout: 3000 }, ); }, 60000); it('renders the NodeValidate tab', async () => { const djClient = mockDJClient(); window.scrollTo = jest.fn(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); djClient.DataJunctionAPI.nodeDimensions.mockReturnValue([]); djClient.DataJunctionAPI.getMetric.mockReturnValue( mocks.mockMetricNodeJson, ); djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); djClient.DataJunctionAPI.sql.mockReturnValue({ sql: 'SELECT * FROM testNode', }); const streamNodeData = { onmessage: jest.fn(), onerror: jest.fn(), close: jest.fn(), }; djClient.DataJunctionAPI.streamNodeData.mockResolvedValue(streamNodeData); djClient.DataJunctionAPI.streamNodeData.mockResolvedValueOnce({ state: 'FINISHED', results: [ { columns: [{ name: 'column1' }, { name: 'column2' }], rows: [ [1, 'value1'], [2, 'value2'], ], }, ], }); const element = ( <DJClientContext.Provider value={djClient}> <NodePage /> </DJClientContext.Provider> ); render( <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/validate']} > <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); await waitFor(() => { expect(screen.getByText('Group By')).toBeInTheDocument(); expect(screen.getByText('Add Filters')).toBeInTheDocument(); expect(screen.getByText('Generated Query')).toBeInTheDocument(); expect(screen.getByText('Results')).toBeInTheDocument(); }); // Click on the 'Validate' tab fireEvent.click(screen.getByRole('button', { name: '► Validate' })); await waitFor(() => { expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith( mocks.mockMetricNode.name, ); expect(djClient.DataJunctionAPI.sql).toHaveBeenCalledWith( mocks.mockMetricNode.name, { dimensions: [], filters: [] }, ); expect(djClient.DataJunctionAPI.nodeDimensions).toHaveBeenCalledWith( mocks.mockMetricNode.name, ); }); // Click on 'Run' to run the node query const runButton = screen.getByText('► Run'); fireEvent.click(runButton); await waitFor(() => { expect(djClient.DataJunctionAPI.streamNodeData).toHaveBeenCalledWith( mocks.mockMetricNode.name, { dimensions: [], filters: [] }, ); expect(streamNodeData.onmessage).toBeDefined(); expect(streamNodeData.onerror).toBeDefined(); }); const infoTab = screen.getByRole('button', { name: 'QueryInfo' }); const resultsTab = screen.getByText('Results'); // Initially, the Results tab should be active expect(resultsTab).toHaveClass('active'); expect(infoTab).not.toHaveClass('active'); // Click on the Info tab first fireEvent.click(infoTab); await waitFor(() => { // Now, the Info tab should be active expect(infoTab).toHaveClass('active'); expect(resultsTab).not.toHaveClass('active'); }); // Click on the Results tab fireEvent.click(resultsTab); await waitFor(() => { // Now, the Results tab should be active again expect(resultsTab).toHaveClass('active'); expect(infoTab).not.toHaveClass('active'); }); }); it('renders a NodeColumnLineage tab correctly', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); djClient.DataJunctionAPI.getMetric.mockReturnValue( mocks.mockMetricNodeJson, ); djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); djClient.DataJunctionAPI.node_lineage.mockReturnValue( mocks.mockNodeLineage, ); const element = ( <DJClientContext.Provider value={djClient}> <NodePage /> </DJClientContext.Provider> ); render( <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/lineage']} > <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); await waitFor(() => { fireEvent.click(screen.getByRole('button', { name: 'Lineage' })); expect(djClient.DataJunctionAPI.node_lineage).toHaveBeenCalledWith( mocks.mockMetricNode.name, ); }); }); it('renders a NodeGraph tab correctly', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); djClient.DataJunctionAPI.getMetric.mockReturnValue( mocks.mockMetricNodeJson, ); djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); const element = ( <DJClientContext.Provider value={djClient}> <NodePage /> </DJClientContext.Provider> ); render( <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/graph']}> <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); await waitFor(() => { fireEvent.click(screen.getByRole('button', { name: 'Graph' })); expect(djClient.DataJunctionAPI.node_dag).toHaveBeenCalledWith( mocks.mockMetricNode.name, ); }); }); it('renders Linked Nodes tab correctly', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockDimensionNode); djClient.DataJunctionAPI.nodesWithDimension.mockReturnValue( mocks.mockLinkedNodes, ); const element = ( <DJClientContext.Provider value={djClient}> <NodePage /> </DJClientContext.Provider> ); render( <MemoryRouter initialEntries={['/nodes/default.dispatcher/linked']}> <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); await waitFor(() => { fireEvent.click(screen.getByRole('button', { name: 'Linked Nodes' })); expect(djClient.DataJunctionAPI.nodesWithDimension).toHaveBeenCalledWith( mocks.mockDimensionNode.name, ); }); }); });