UNPKG

datajunction-ui

Version:
844 lines (784 loc) 28.1 kB
import * as domTestingLib from '@testing-library/dom'; 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 directly (not via Loadable) — Loadable wraps it in a // React.lazy + Suspense boundary which adds an async resolution step that // can race testing-library's waitFor on slower CI runners. import { NodePage } from '../index'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import userEvent from '@testing-library/user-event'; // Mock cronstrue for NodePreAggregationsTab vi.mock('cronstrue', () => { const mock = { toString: () => 'Every day at midnight' }; return { ...mock, default: mock }; }); // Mock CSS imports vi.mock('../../../../styles/preaggregations.css', () => ({})); // Mock recharts for NodeDataFlowTab (Sankey doesn't work in jsdom) vi.mock('recharts', () => ({ Sankey: () => <div data-testid="sankey-chart" />, Tooltip: () => null, })); // ResizeObserver is not defined in jsdom global.ResizeObserver = function (callback) { return { observe: () => {}, disconnect: () => {}, unobserve: () => {} }; }; // jsdom doesn't implement canvas — stub getContext so text-measurement code doesn't crash HTMLCanvasElement.prototype.getContext = () => ({ font: '', measureText: () => ({ width: 0 }), }); describe('<NodePage />', () => { /* moved to top */ 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: vi.fn(), metric: vi.fn(), getMetric: vi.fn(), revalidate: vi.fn().mockReturnValue({ status: 'valid' }), node_dag: vi.fn().mockReturnValue(mocks.mockNodeDAG), clientCode: vi.fn().mockResolvedValue('dj_client = DJClient()'), columns: vi.fn(), history: vi.fn(), revisions: vi.fn(), materializations: vi.fn(), availabilityStates: vi.fn(), refreshLatestMaterialization: vi.fn(), materializationInfo: vi.fn(), sql: vi.fn(), cube: vi.fn(), compiledSql: vi.fn(), node_lineage: vi.fn(), nodesWithDimension: vi.fn(), attributes: vi.fn(), dimensions: vi.fn(), setPartition: vi.fn(), engines: vi.fn(), streamNodeData: vi.fn(), nodeDimensions: vi.fn(), getNotificationPreferences: vi.fn().mockResolvedValue([]), subscribeToNotifications: vi.fn().mockResolvedValue({ status: 200 }), unsubscribeFromNotifications: vi .fn() .mockResolvedValue({ status: 200 }), setAttributes: vi.fn().mockResolvedValue({ status: 200 }), linkDimension: vi.fn().mockResolvedValue({ status: 200 }), unlinkDimension: vi.fn().mockResolvedValue({ status: 200 }), addReferenceDimensionLink: vi.fn().mockResolvedValue({ status: 200 }), removeReferenceDimensionLink: vi .fn() .mockResolvedValue({ status: 200 }), addComplexDimensionLink: vi.fn().mockResolvedValue({ status: 200 }), removeComplexDimensionLink: vi.fn().mockResolvedValue({ status: 200 }), listPreaggs: vi.fn().mockResolvedValue({ items: [] }), deactivatePreaggWorkflow: vi.fn().mockResolvedValue({ status: 200 }), namespaceSources: vi.fn().mockResolvedValue(null), listDeployments: vi.fn().mockResolvedValue([]), getNamespaceGitConfig: vi.fn().mockResolvedValue(null), upstreamsGQL: vi.fn().mockResolvedValue([]), downstreamsGQL: vi.fn().mockResolvedValue([]), findCubesWithMetrics: vi.fn().mockResolvedValue([]), }, }; }; 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.mockResolvedValue( 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'); }, { timeout: 4000 }, ); // Wait separately for getMetric to be called and data to render await waitFor(() => { expect(djClient.DataJunctionAPI.getMetric).toHaveBeenCalledWith( 'default.num_repair_orders', ); }); // Wait for metric expression to appear (SyntaxHighlighter may split text) await waitFor(() => { expect(screen.getByText(/count/)).toBeInTheDocument(); }); expect(container.getElementsByClassName('language-sql')).toMatchSnapshot(); }, 60000); it('hides Edit and shows the read-only badge for a node in a read-only (flat git) namespace', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); djClient.DataJunctionAPI.getMetric.mockResolvedValue( mocks.mockMetricNodeJson, ); // magnesium.tech is a flat git-backed namespace (github_repo_path + // git_branch, no parent) → read-only via shape, even though git_only is // not set. Regression guard: node pages must honor shape-based read-only, // not just the git_only flag. djClient.DataJunctionAPI.getNamespaceGitConfig.mockResolvedValue({ github_repo_path: 'corp/tech-realtime-guardrails', git_branch: 'main', git_root_namespace: 'magnesium.tech', }); const element = ( <DJClientContext.Provider value={djClient}> <NodePage {...defaultProps} /> </DJClientContext.Provider> ); render( <MemoryRouter initialEntries={['/nodes/magnesium.tech.withinapp_ttr_log/info']} > <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); // The read-only badge appears once the namespace git config resolves... expect(await screen.findByLabelText('ReadOnly')).toBeInTheDocument(); // ...and the Edit button is not offered. expect( screen.queryByRole('button', { name: 'Edit' }), ).not.toBeInTheDocument(); }, 60000); it('renders the NodeInfo tab correctly for cube nodes', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockCubeNode); djClient.DataJunctionAPI.cube.mockResolvedValue(mocks.mockCubesCube); const element = ( <DJClientContext.Provider value={djClient}> <NodePage {...defaultProps} /> </DJClientContext.Provider> ); const { container } = render( <MemoryRouter initialEntries={['/nodes/default.repair_orders_cube/info']}> <Routes> <Route path="nodes/:name/:tab" 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.mockResolvedValue(mocks.mockMetricNode); djClient.DataJunctionAPI.getMetric.mockResolvedValue( mocks.mockMetricNodeJson, ); djClient.DataJunctionAPI.columns.mockResolvedValue(mocks.metricNodeColumns); djClient.DataJunctionAPI.attributes.mockResolvedValue(mocks.attributes); djClient.DataJunctionAPI.dimensions.mockResolvedValue(mocks.dimensions); djClient.DataJunctionAPI.engines.mockReturnValue([]); djClient.DataJunctionAPI.setPartition.mockResolvedValue({ 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( expect.objectContaining({ name: mocks.mockMetricNode.name }), ); 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 manage dimension links dialog can be opened const manageDimensionLinksButton = screen.getByRole('button', { name: 'ManageDimensionLinksToggle', }); expect(manageDimensionLinksButton).toBeInTheDocument(); fireEvent.click(manageDimensionLinksButton); expect( screen.getByRole('dialog', { name: 'ManageDimensionLinksDialog' }), ).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); it('renders the NodeHistory tab correctly', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); djClient.DataJunctionAPI.getMetric.mockResolvedValue( 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 an empty NodeMaterialization tab correctly', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockResolvedValue(mocks.mockMetricNode); djClient.DataJunctionAPI.getMetric.mockResolvedValue( mocks.mockMetricNodeJson, ); djClient.DataJunctionAPI.columns.mockResolvedValue(mocks.metricNodeColumns); // For metric nodes, listPreaggs is called, not materializations djClient.DataJunctionAPI.listPreaggs.mockResolvedValue({ items: [] }); 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>, ); // For metric nodes, NodePreAggregationsTab is used, which calls listPreaggs await waitFor(() => { expect(djClient.DataJunctionAPI.listPreaggs).toHaveBeenCalledWith({ node_name: mocks.mockMetricNode.name, include_stale: true, }); }); // Check for the empty state text (for NodePreAggregationsTab) expect( screen.getByText('No pre-aggregations found for this node.'), ).toBeInTheDocument(); }); it('renders the NodeMaterialization tab with materializations correctly', async () => { const djClient = mockDJClient(); // Use cube node - only cubes use NodeMaterializationTab // Override columns with explicit partition: null to avoid undefined.type_ error const cubeNodeWithPartitions = { ...mocks.mockCubeNode, columns: mocks.mockCubeNode.columns.map(col => ({ ...col, partition: null, })), }; djClient.DataJunctionAPI.node.mockResolvedValue(cubeNodeWithPartitions); djClient.DataJunctionAPI.cube.mockResolvedValue(mocks.mockCubesCube); djClient.DataJunctionAPI.columns.mockResolvedValue([]); djClient.DataJunctionAPI.materializations.mockResolvedValue( mocks.nodeMaterializations, ); djClient.DataJunctionAPI.availabilityStates.mockResolvedValue([]); djClient.DataJunctionAPI.materializationInfo.mockResolvedValue( mocks.materializationInfo, ); const element = ( <DJClientContext.Provider value={djClient}> <NodePage /> </DJClientContext.Provider> ); render( <MemoryRouter initialEntries={['/nodes/default.repair_orders_cube/materializations']} > <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); // Wait for the node to load first await waitFor(() => { expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith( 'default.repair_orders_cube', ); }); // Then wait for materializations to be fetched await waitFor(() => { expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith( mocks.mockCubeNode.name, ); }); }); it('renders a NodeColumnLineage tab correctly', async () => { const djClient = mockDJClient(); djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); djClient.DataJunctionAPI.getMetric.mockResolvedValue( 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.mockResolvedValue( 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/data-flow']} > <Routes> <Route path="nodes/:name/:tab" element={element} /> </Routes> </MemoryRouter>, ); await waitFor(() => { fireEvent.click(screen.getByRole('button', { name: 'Data Flow' })); expect(djClient.DataJunctionAPI.upstreamsGQL).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, ); }); }); });