datajunction-ui
Version:
DataJunction UI
516 lines (464 loc) • 14.5 kB
JSX
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import DJClientContext from '../../../providers/djclient';
import { CubeBuilderPage } from '../index';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import React from 'react';
const mockDjClient = {
metrics: vi.fn(),
searchMetrics: vi.fn(),
getMetricsInfo: vi.fn(),
commonDimensions: vi.fn(),
createCube: vi.fn(),
namespaces: vi.fn(),
cube: vi.fn(),
getCubeForEditing: vi.fn(),
node: vi.fn(),
listTags: vi.fn(),
tagsNode: vi.fn(),
patchCube: vi.fn(),
users: vi.fn(),
whoami: vi.fn(),
metricsV3: vi.fn(),
};
const mockMetrics = [
'default.num_repair_orders',
'default.avg_repair_price',
'default.total_repair_cost',
];
const mockCube = {
name: 'default.repair_orders_cube',
type: 'CUBE',
owners: [
{
username: 'someone@example.com',
},
],
current: {
displayName: 'Default: Repair Orders Cube',
description: 'Repairs cube',
mode: 'DRAFT',
cubeMetrics: [
{
name: 'default.total_repair_cost',
},
{
name: 'default.num_repair_orders',
},
],
cubeDimensions: [
{
name: 'default.hard_hat.country',
attribute: 'country',
properties: ['dimension'],
},
{
name: 'default.hard_hat.state',
attribute: 'state',
properties: ['dimension'],
},
],
},
tags: [
{
name: 'repairs',
displayName: 'Repairs Domain',
},
],
};
const mockCommonDimensions = [
{
name: 'default.date_dim.dateint',
type: 'timestamp',
node_name: 'default.date_dim',
node_display_name: 'Date',
properties: [],
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',
node_name: 'default.date_dim',
node_display_name: 'Date',
properties: [],
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',
node_name: 'default.date_dim',
node_display_name: 'Date',
properties: [],
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',
node_name: 'default.date_dim',
node_display_name: 'Date',
properties: [],
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',
node_name: 'default.date_dim',
node_display_name: 'Date',
properties: [],
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',
node_name: 'default.date_dim',
node_display_name: 'Date',
properties: [],
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',
node_name: 'default.date_dim',
node_display_name: 'Date',
properties: [],
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',
node_name: 'default.date_dim',
node_display_name: 'Date',
properties: [],
path: [
'default.repair_order_details.repair_order_id',
'default.repair_order.hard_hat_id',
'default.hard_hat.hire_date',
],
},
];
const mockSearchMetricsResults = mockMetrics.map(m => ({
value: m,
label: m,
}));
describe('CubeBuilderPage', () => {
beforeEach(() => {
mockDjClient.metrics.mockResolvedValue(mockMetrics);
mockDjClient.searchMetrics.mockResolvedValue(mockSearchMetricsResults);
mockDjClient.commonDimensions.mockResolvedValue(mockCommonDimensions);
mockDjClient.createCube.mockResolvedValue({ status: 201, json: {} });
mockDjClient.namespaces.mockResolvedValue(['default']);
mockDjClient.getCubeForEditing.mockResolvedValue(mockCube);
mockDjClient.listTags.mockResolvedValue([]);
mockDjClient.tagsNode.mockResolvedValue([]);
mockDjClient.patchCube.mockResolvedValue({ status: 201, json: {} });
mockDjClient.users.mockResolvedValue([{ username: 'dj' }]);
mockDjClient.whoami.mockResolvedValue({ username: 'dj' });
mockDjClient.getMetricsInfo.mockResolvedValue([]);
mockDjClient.metricsV3.mockResolvedValue({ sql: '', errors: [] });
window.scrollTo = vi.fn();
});
afterEach(() => {
vi.clearAllMocks();
});
it('renders without crashing', async () => {
render(
<MemoryRouter>
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
<CubeBuilderPage />
</DJClientContext.Provider>
</MemoryRouter>,
);
expect(screen.getByText('Cube')).toBeInTheDocument();
});
it('renders the Metrics section', async () => {
render(
<MemoryRouter>
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
<CubeBuilderPage />
</DJClientContext.Provider>
</MemoryRouter>,
);
expect(
screen.getByRole('heading', { name: 'Metrics' }),
).toBeInTheDocument();
});
it('renders the Dimensions section', async () => {
render(
<MemoryRouter>
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
<CubeBuilderPage />
</DJClientContext.Provider>
</MemoryRouter>,
);
expect(
screen.getByRole('heading', { name: 'Dimensions' }),
).toBeInTheDocument();
});
it('shows the Create Cube submit button in Add mode', async () => {
render(
<MemoryRouter>
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
<CubeBuilderPage />
</DJClientContext.Provider>
</MemoryRouter>,
);
expect(
await screen.findByRole('button', { name: 'CreateCube' }),
).toHaveTextContent('Create Cube');
});
it('renders the Edit page with prefilled fields and saves via patchCube', async () => {
render(
<MemoryRouter
initialEntries={['/nodes/default.repair_orders_cube/edit-cube']}
>
<Routes>
<Route
path="nodes/:name/edit-cube"
element={
<DJClientContext.Provider
value={{ DataJunctionAPI: mockDjClient }}
>
<CubeBuilderPage />
</DJClientContext.Provider>
}
/>
</Routes>
</MemoryRouter>,
);
// The Edit branch fetches the cube and renders its name + display name.
await waitFor(() =>
expect(mockDjClient.getCubeForEditing).toHaveBeenCalledWith(
'default.repair_orders_cube',
),
);
expect(
await screen.findByText('default.repair_orders_cube'),
).toBeInTheDocument();
// Heading reads "Edit" in edit mode.
expect(screen.getByRole('heading', { name: /Edit/ })).toBeInTheDocument();
// Save button reads "Save" in edit mode.
const saveButton = screen.getByRole('button', { name: 'CreateCube' });
expect(saveButton).toHaveTextContent('Save');
fireEvent.click(saveButton);
await waitFor(() => expect(mockDjClient.patchCube).toHaveBeenCalled(), {
timeout: 1500,
});
});
it('shows a save error when patchCube fails', async () => {
mockDjClient.patchCube.mockResolvedValueOnce({
status: 500,
json: { message: 'something exploded' },
});
render(
<MemoryRouter
initialEntries={['/nodes/default.repair_orders_cube/edit-cube']}
>
<Routes>
<Route
path="nodes/:name/edit-cube"
element={
<DJClientContext.Provider
value={{ DataJunctionAPI: mockDjClient }}
>
<CubeBuilderPage />
</DJClientContext.Provider>
}
/>
</Routes>
</MemoryRouter>,
);
await waitFor(() =>
expect(mockDjClient.getCubeForEditing).toHaveBeenCalled(),
);
fireEvent.click(screen.getByRole('button', { name: 'CreateCube' }));
expect(
await screen.findByText(/something exploded/, {}, { timeout: 1500 }),
).toBeInTheDocument();
});
// TODO: Update test for async metrics search
it.skip('creates a new cube', async () => {
render(
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
<CubeBuilderPage />
</DJClientContext.Provider>,
);
const selectMetrics = screen.getAllByTestId('select-metrics')[0];
expect(selectMetrics).toBeDefined();
expect(selectMetrics).not.toBeNull();
// Type to search for metrics (async select requires typing)
const metricsInput = selectMetrics.querySelector('input');
fireEvent.change(metricsInput, { target: { value: 'default' } });
// Advance timers to flush the 300ms debounce
vi.advanceTimersByTime(400);
// Wait for search results
await waitFor(() => {
expect(mockDjClient.searchMetrics).toHaveBeenCalled();
});
// Wait for options to appear and click each one
for (const metric of mockMetrics) {
await waitFor(() => {
expect(screen.getByText(metric)).toBeInTheDocument();
});
fireEvent.click(screen.getByText(metric));
}
fireEvent.click(screen.getAllByText('Dimensions *')[0]);
// Wait for commonDimensions to be called and state to update
await waitFor(() => {
expect(mockDjClient.commonDimensions).toHaveBeenCalled();
});
const selectDimensions = screen.getAllByTestId('select-dimensions')[0];
expect(selectDimensions).toBeDefined();
expect(selectDimensions).not.toBeNull();
await waitFor(() => {
expect(
screen.getByText(
'default.repair_order_details.repair_order_id → default.repair_order.hard_hat_id → default.hard_hat.birth_date',
),
).toBeInTheDocument();
});
const selectDimensionsDate = screen.getAllByTestId(
'dimensions-default.date_dim',
)[0];
fireEvent.keyDown(selectDimensionsDate.firstChild, { key: 'ArrowDown' });
await waitFor(() => {
expect(screen.getByText('Day')).toBeInTheDocument();
});
fireEvent.click(screen.getByText('Day'));
fireEvent.click(screen.getByText('Month'));
fireEvent.click(screen.getByText('Year'));
fireEvent.click(screen.getByText('Dateint'));
// Save
const createCube = screen.getAllByRole('button', {
name: 'CreateCube',
})[0];
expect(createCube).toBeInTheDocument();
fireEvent.click(createCube);
await waitFor(() => {
expect(mockDjClient.createCube).toHaveBeenCalledWith(
'',
'',
'',
'published',
[
'default.num_repair_orders',
'default.avg_repair_price',
'default.total_repair_cost',
],
[
'default.date_dim.day',
'default.date_dim.month',
'default.date_dim.year',
'default.date_dim.dateint',
],
[],
);
});
});
const renderEditNode = element => {
return render(
<MemoryRouter
initialEntries={['/nodes/default.repair_orders_cube/edit-cube']}
>
<Routes>
<Route path="nodes/:name/edit-cube" element={element} />
</Routes>
</MemoryRouter>,
);
};
// TODO: Update test for async metrics search
it.skip('updates an existing cube', async () => {
renderEditNode(
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
<CubeBuilderPage />
</DJClientContext.Provider>,
);
expect(screen.getAllByText('Edit')[0]).toBeInTheDocument();
await waitFor(() => {
expect(mockDjClient.getCubeForEditing).toHaveBeenCalled();
});
// In edit mode, existing metrics are pre-populated from cube data
const selectMetrics = screen.getAllByTestId('select-metrics')[0];
expect(selectMetrics).toBeDefined();
expect(selectMetrics).not.toBeNull();
// Wait for cube metrics to be loaded
await waitFor(() => {
expect(screen.getByText('default.num_repair_orders')).toBeInTheDocument();
});
fireEvent.click(screen.getAllByText('Dimensions *')[0]);
// Wait for commonDimensions to be called and state to update
await waitFor(() => {
expect(mockDjClient.commonDimensions).toHaveBeenCalled();
});
const selectDimensions = screen.getAllByTestId('select-dimensions')[0];
expect(selectDimensions).toBeDefined();
expect(selectDimensions).not.toBeNull();
await waitFor(() => {
expect(
screen.getByText(
'default.repair_order_details.repair_order_id → default.repair_order.hard_hat_id → default.hard_hat.birth_date',
),
).toBeInTheDocument();
});
const selectDimensionsDate = screen.getAllByTestId(
'dimensions-default.date_dim',
)[0];
fireEvent.keyDown(selectDimensionsDate.firstChild, { key: 'ArrowDown' });
await waitFor(() => {
expect(screen.getByText('Day')).toBeInTheDocument();
});
fireEvent.click(screen.getByText('Day'));
fireEvent.click(screen.getByText('Month'));
fireEvent.click(screen.getByText('Year'));
fireEvent.click(screen.getByText('Dateint'));
// Save
const createCube = screen.getAllByRole('button', {
name: 'CreateCube',
})[0];
expect(createCube).toBeInTheDocument();
fireEvent.click(createCube);
await waitFor(() => {
expect(mockDjClient.patchCube).toHaveBeenCalledWith(
'default.repair_orders_cube',
'Default: Repair Orders Cube',
'Repairs cube',
'draft',
['default.total_repair_cost', 'default.num_repair_orders'],
[
'default.date_dim.day',
'default.date_dim.month',
'default.date_dim.year',
'default.date_dim.dateint',
],
[],
[],
);
});
});
});