datajunction-ui
Version:
DataJunction UI
1,691 lines (1,453 loc) • 102 kB
JSX
import {
render,
screen,
fireEvent,
waitFor,
act,
} from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import {
QueryOverviewPanel,
PreAggDetailsPanel,
MetricDetailsPanel,
getDimensionNodeName,
normalizeGrain,
getScheduleSummary,
getStatusInfo,
inferGranularity,
formatBytes,
getScanWarningLevel,
formatScanEstimate,
} from '../PreAggDetailsPanel';
import React from 'react';
// Mock the syntax highlighter to avoid issues with CSS imports
vi.mock('react-syntax-highlighter', () => ({
Light: ({ children }) => (
<pre data-testid="syntax-highlighter">{children}</pre>
),
}));
vi.mock('react-syntax-highlighter/src/styles/hljs', () => ({
atomOneLight: {},
}));
// Mock clipboard API
Object.assign(navigator, {
clipboard: {
writeText: vi.fn(),
},
});
const mockMeasuresResult = {
grain_groups: [
{
parent_name: 'default.repair_orders',
aggregability: 'FULL',
grain: ['date_id', 'customer_id'],
components: [
{
name: 'sum_revenue',
expression: 'SUM(revenue)',
aggregation: 'SUM',
merge: 'SUM',
},
{
name: 'count_orders',
expression: 'COUNT(*)',
aggregation: 'COUNT',
merge: 'SUM',
},
],
sql: 'SELECT date_id, customer_id, SUM(revenue) FROM orders GROUP BY 1, 2',
},
{
parent_name: 'inventory.stock',
aggregability: 'LIMITED',
grain: ['warehouse_id'],
components: [
{
name: 'sum_quantity',
expression: 'SUM(quantity)',
aggregation: 'SUM',
merge: 'SUM',
},
],
},
],
metric_formulas: [
{
name: 'default.num_repair_orders',
short_name: 'num_repair_orders',
combiner: 'SUM(count_orders)',
is_derived: false,
components: ['count_orders'],
},
{
name: 'default.avg_repair_price',
short_name: 'avg_repair_price',
combiner: 'SUM(sum_revenue) / SUM(count_orders)',
is_derived: true,
components: ['sum_revenue', 'count_orders'],
},
],
};
const mockMetricsResult = {
sql: 'SELECT date_id, SUM(revenue) as total_revenue FROM orders GROUP BY 1',
};
const renderWithRouter = component => {
return render(<MemoryRouter>{component}</MemoryRouter>);
};
describe('QueryOverviewPanel', () => {
const defaultProps = {
measuresResult: mockMeasuresResult,
metricsResult: mockMetricsResult,
selectedMetrics: ['default.num_repair_orders', 'default.avg_repair_price'],
selectedDimensions: [
'default.date_dim.dateint',
'default.customer.country',
],
};
describe('Empty States', () => {
it('shows hint when no metrics selected', () => {
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
selectedMetrics={[]}
selectedDimensions={[]}
/>,
);
expect(screen.getByText('Query Planner')).toBeInTheDocument();
expect(
screen.getByText(/Select metrics and dimensions/),
).toBeInTheDocument();
});
it('shows loading state when results are pending', () => {
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
measuresResult={null}
metricsResult={null}
/>,
);
expect(screen.getByText('Building query plan...')).toBeInTheDocument();
});
});
describe('Header', () => {
it('renders the overview header', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText('Query Plan')).toBeInTheDocument();
});
it('shows metric and dimension counts', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText('2 metrics × 2 dimensions')).toBeInTheDocument();
});
});
describe('Pre-Aggregations Summary', () => {
it('displays pre-aggregations section', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText(/Pre-Aggregations/)).toBeInTheDocument();
});
it('shows correct count of pre-aggregations', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText('Pre-Aggregations (2)')).toBeInTheDocument();
});
it('displays pre-agg source names', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText('repair_orders')).toBeInTheDocument();
expect(screen.getByText('stock')).toBeInTheDocument();
});
it('shows status badge for each pre-agg', () => {
// The updated UI shows status badges instead of aggregability badges
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
// All pre-aggs without materialization config show "Not Set" status
expect(screen.getAllByText('○ Not Set').length).toBe(2);
});
it('displays grain columns', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText('date_id, customer_id')).toBeInTheDocument();
expect(screen.getByText('warehouse_id')).toBeInTheDocument();
});
it('shows materialization status', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
// Status shows "Not Set" when no materialization is configured
expect(screen.getAllByText('○ Not Set').length).toBe(2);
});
});
describe('Metrics Summary', () => {
it('displays metrics section', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText(/Metrics \(2\)/)).toBeInTheDocument();
});
it('shows metric short names', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
expect(screen.getByText('avg_repair_price')).toBeInTheDocument();
});
it('shows derived badge for derived metrics', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText('Derived')).toBeInTheDocument();
});
it('renders metric links', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
const links = screen.getAllByRole('link');
expect(
links.some(
link =>
link.getAttribute('href') === '/nodes/default.num_repair_orders',
),
).toBe(true);
});
});
describe('Dimensions Summary', () => {
it('displays dimensions section', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText(/Dimensions \(2\)/)).toBeInTheDocument();
});
it('shows dimension short names', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText('dateint')).toBeInTheDocument();
expect(screen.getByText('country')).toBeInTheDocument();
});
it('renders dimension links', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
const links = screen.getAllByRole('link');
expect(
links.some(
link => link.getAttribute('href') === '/nodes/default.date_dim',
),
).toBe(true);
});
});
describe('SQL Section', () => {
it('displays generated SQL section', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText('Generated SQL')).toBeInTheDocument();
});
it('shows SQL view toggle with Optimized and Raw options', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByText('Optimized')).toBeInTheDocument();
expect(screen.getByText('Raw')).toBeInTheDocument();
});
it('renders SQL in syntax highlighter', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
expect(screen.getByTestId('syntax-highlighter')).toBeInTheDocument();
expect(screen.getByText(mockMetricsResult.sql)).toBeInTheDocument();
});
it('defaults to Optimized view', () => {
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
const optimizedBtn = screen.getByText('Optimized');
expect(optimizedBtn).toHaveClass('active');
});
it('fetches and displays raw SQL when Raw tab is clicked', async () => {
const mockRawSql =
'SELECT * FROM raw_table WHERE date_id = 123 GROUP BY 1';
const onFetchRawSql = vi.fn().mockResolvedValue(mockRawSql);
renderWithRouter(
<QueryOverviewPanel {...defaultProps} onFetchRawSql={onFetchRawSql} />,
);
// Click Raw tab
const rawBtn = screen.getByText('Raw');
await act(async () => {
fireEvent.click(rawBtn);
});
await waitFor(() => {
expect(onFetchRawSql).toHaveBeenCalled();
});
});
});
describe('Materialization CTA', () => {
it('shows Configure button when not materialized', () => {
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
/>,
);
expect(screen.getByText('Configure')).toBeInTheDocument();
});
it('shows CTA content with Ready to materialize text', () => {
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
/>,
);
expect(screen.getByText('Ready to materialize?')).toBeInTheDocument();
});
it('opens configuration form when Configure button is clicked', async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [],
});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
// Should show configuration form
await waitFor(() => {
expect(
screen.getByText('Configure Materialization'),
).toBeInTheDocument();
});
});
});
describe('Materialization Configuration Form', () => {
const setupConfigForm = async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [
{ name: 'date_id', type: 'int' },
{ name: 'customer_id', type: 'int' },
],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
const onPlanMaterialization = vi.fn().mockResolvedValue({});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={onPlanMaterialization}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(
screen.getByText('Configure Materialization'),
).toBeInTheDocument();
});
return { onPlanMaterialization, onFetchNodePartitions };
};
it('shows strategy options (Full and Incremental)', async () => {
await setupConfigForm();
expect(screen.getByText('Strategy')).toBeInTheDocument();
expect(screen.getByText('Full')).toBeInTheDocument();
expect(screen.getByText('Incremental')).toBeInTheDocument();
});
it('allows switching strategy to Full', async () => {
await setupConfigForm();
const fullRadio = screen.getByLabelText('Full');
fireEvent.click(fullRadio);
expect(fullRadio).toBeChecked();
});
it('shows close button on configuration form', async () => {
await setupConfigForm();
// Close button is the × in the header
const closeButtons = screen.getAllByText('×');
expect(closeButtons.length).toBeGreaterThan(0);
});
it('closes configuration form when close button is clicked', async () => {
await setupConfigForm();
// Find and click the close button
const closeButton = screen.getByRole('button', {
name: /×/,
});
fireEvent.click(closeButton);
// Configuration form should be closed
await waitFor(() => {
expect(
screen.queryByText('Configure Materialization'),
).not.toBeInTheDocument();
});
});
});
describe('Error Display', () => {
it('shows materialization error when present', () => {
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
materializationError="Failed to plan materialization"
onClearError={vi.fn()}
/>,
);
expect(
screen.getByText('Failed to plan materialization'),
).toBeInTheDocument();
});
it('calls onClearError when dismiss button is clicked', () => {
const onClearError = vi.fn();
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
materializationError="Failed to plan materialization"
onClearError={onClearError}
/>,
);
// Find dismiss button (aria-label="Dismiss error")
const dismissBtn = screen.getByLabelText('Dismiss error');
fireEvent.click(dismissBtn);
expect(onClearError).toHaveBeenCalled();
});
});
describe('Workflow URLs Display', () => {
it('displays workflow URLs when provided', () => {
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
workflowUrls={['http://workflow.example.com/job/123']}
onClearWorkflowUrls={vi.fn()}
/>,
);
// The workflow URL or a related indicator should be shown
expect(screen.getByText(/workflow/i)).toBeInTheDocument();
});
});
describe('Planned Pre-aggregations', () => {
it('shows status badges for planned pre-aggs', () => {
const plannedPreaggs = {
'default.repair_orders|customer_id,date_id': {
id: 1,
node_name: 'default.repair_orders',
grain_columns: ['date_id', 'customer_id'],
workflow_status: 'active',
workflow_urls: ['http://example.com'],
},
};
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
plannedPreaggs={plannedPreaggs}
onPlanMaterialization={vi.fn()}
/>,
);
// Should show some indicator of planned state
// The exact text depends on the status logic
expect(
screen.queryByText('Not Set') || screen.queryByText('Workflow Active'),
).toBeTruthy();
});
});
describe('Loaded Cube Display', () => {
it('shows cube name banner when loadedCubeName is provided', () => {
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
loadedCubeName="default.test_cube"
onPlanMaterialization={vi.fn()}
/>,
);
// Should show some indication of loaded cube
// The exact text depends on the implementation
expect(screen.getByText(/Query Plan/)).toBeInTheDocument();
});
});
describe('Partition Setup Form', () => {
const setupPartitionTest = async (partitionResults = {}) => {
const mockNodePartitions = {
'default.repair_orders': {
columns: [
{ name: 'date_id', type: 'int' },
{ name: 'customer_id', type: 'int' },
{ name: 'dateint', type: 'int' },
],
temporalPartitions: [],
...partitionResults,
},
};
const onFetchNodePartitions = vi.fn().mockImplementation(nodeName =>
Promise.resolve(
mockNodePartitions[nodeName] || {
columns: [],
temporalPartitions: [],
},
),
);
const onSetPartition = vi.fn().mockResolvedValue({ status: 200 });
const onPlanMaterialization = vi.fn().mockResolvedValue({});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={onPlanMaterialization}
onFetchNodePartitions={onFetchNodePartitions}
onSetPartition={onSetPartition}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(
screen.getByText('Configure Materialization'),
).toBeInTheDocument();
});
return { onFetchNodePartitions, onSetPartition, onPlanMaterialization };
};
it('shows partition setup form when incremental is selected and no temporal partitions', async () => {
await setupPartitionTest();
// Select incremental strategy
const incrementalRadio = screen.getByLabelText('Incremental');
await act(async () => {
fireEvent.click(incrementalRadio);
});
// Should show partition setup header
await waitFor(() => {
expect(
screen.getByText('Set up temporal partitions for incremental builds'),
).toBeInTheDocument();
});
});
it('shows column dropdown with date-like columns prioritized', async () => {
await setupPartitionTest();
// Select incremental strategy
const incrementalRadio = screen.getByLabelText('Incremental');
await act(async () => {
fireEvent.click(incrementalRadio);
});
await waitFor(() => {
// Multiple Column labels may exist (one per node)
const columnLabels = screen.getAllByText('Column');
expect(columnLabels.length).toBeGreaterThan(0);
});
// Should have date-like columns with star markers
expect(screen.getByText(/dateint.*★/)).toBeInTheDocument();
expect(screen.getByText(/date_id.*★/)).toBeInTheDocument();
});
it('shows granularity dropdown with Day, Hour, Month options', async () => {
await setupPartitionTest();
const incrementalRadio = screen.getByLabelText('Incremental');
await act(async () => {
fireEvent.click(incrementalRadio);
});
await waitFor(() => {
const granularityLabels = screen.getAllByText('Granularity');
expect(granularityLabels.length).toBeGreaterThan(0);
});
// Check granularity options exist - use getAllByRole since there may be multiple
const granularitySelects = screen.getAllByRole('combobox');
// Find the one with Day selected
const daySelect = granularitySelects.find(
select => select.value === 'day',
);
expect(daySelect).toBeInTheDocument();
});
it('shows format input field with placeholder', async () => {
await setupPartitionTest();
const incrementalRadio = screen.getByLabelText('Incremental');
await act(async () => {
fireEvent.click(incrementalRadio);
});
await waitFor(() => {
const formatLabels = screen.getAllByText('Format');
expect(formatLabels.length).toBeGreaterThan(0);
});
// Format input should have placeholder - there may be multiple
const formatInputs = screen.getAllByPlaceholderText('yyyyMMdd');
expect(formatInputs.length).toBeGreaterThan(0);
});
it('disables Set button when no column is selected', async () => {
await setupPartitionTest();
const incrementalRadio = screen.getByLabelText('Incremental');
await act(async () => {
fireEvent.click(incrementalRadio);
});
await waitFor(() => {
// Wait for partition setup form to show
expect(
screen.getByText('Set up temporal partitions for incremental builds'),
).toBeInTheDocument();
});
// Set button should be disabled when no column selected (initially empty)
const setBtns = screen.getAllByText('Set');
// At least one Set button should be disabled
const hasDisabledBtn = setBtns.some(btn => btn.disabled);
expect(hasDisabledBtn).toBe(true);
});
it('enables Set button when column is selected', async () => {
await setupPartitionTest();
const incrementalRadio = screen.getByLabelText('Incremental');
await act(async () => {
fireEvent.click(incrementalRadio);
});
await waitFor(() => {
const columnLabels = screen.getAllByText('Column');
expect(columnLabels.length).toBeGreaterThan(0);
});
// Select a column in the first dropdown
const columnSelects = screen.getAllByRole('combobox');
await act(async () => {
fireEvent.change(columnSelects[0], { target: { value: 'date_id' } });
});
// Set button should now be enabled
const setBtns = screen.getAllByText('Set');
expect(setBtns[0]).not.toBeDisabled();
});
it('calls onSetPartition when Set button is clicked', async () => {
const { onSetPartition } = await setupPartitionTest();
const incrementalRadio = screen.getByLabelText('Incremental');
await act(async () => {
fireEvent.click(incrementalRadio);
});
await waitFor(() => {
const columnLabels = screen.getAllByText('Column');
expect(columnLabels.length).toBeGreaterThan(0);
});
// Select a column
const columnSelects = screen.getAllByRole('combobox');
await act(async () => {
fireEvent.change(columnSelects[0], { target: { value: 'date_id' } });
});
// Click Set
const setBtns = screen.getAllByText('Set');
await act(async () => {
fireEvent.click(setBtns[0]);
});
await waitFor(() => {
expect(onSetPartition).toHaveBeenCalledWith(
'default.repair_orders',
'date_id',
'temporal',
'yyyyMMdd',
'day',
);
});
});
it('shows success state when partition is already configured', async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(
screen.getByText('Configure Materialization'),
).toBeInTheDocument();
});
// When partition already exists, the incremental strategy should default to enabled
// and partition setup form should NOT be shown
await waitFor(() => {
// The incremental option should show the partition name badge
// If partition setup prompt is NOT shown, it means partition is configured
expect(
screen.queryByText(
'Set up temporal partitions for incremental builds',
),
).not.toBeInTheDocument();
});
});
});
describe('Backfill Date Range in Config Form', () => {
const setupBackfillTest = async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
const onPlanMaterialization = vi.fn().mockResolvedValue({});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={onPlanMaterialization}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(
screen.getByText('Configure Materialization'),
).toBeInTheDocument();
});
return { onPlanMaterialization };
};
it('shows "Run initial backfill" checkbox for incremental strategy', async () => {
await setupBackfillTest();
// Incremental should be the default when partition exists
await waitFor(() => {
expect(screen.getByText('Run initial backfill')).toBeInTheDocument();
});
});
it('shows backfill date range when checkbox is checked', async () => {
await setupBackfillTest();
await waitFor(() => {
expect(screen.getByText('Run initial backfill')).toBeInTheDocument();
});
// Checkbox should be checked by default
const checkbox = screen.getByRole('checkbox', {
name: /Run initial backfill/i,
});
expect(checkbox).toBeChecked();
// Date range should be visible
expect(screen.getByText('Backfill Date Range')).toBeInTheDocument();
expect(screen.getByText('From')).toBeInTheDocument();
expect(screen.getByText('To')).toBeInTheDocument();
});
it('hides backfill date range when checkbox is unchecked', async () => {
await setupBackfillTest();
await waitFor(() => {
expect(screen.getByText('Run initial backfill')).toBeInTheDocument();
});
// Uncheck the checkbox
const checkbox = screen.getByRole('checkbox', {
name: /Run initial backfill/i,
});
await act(async () => {
fireEvent.click(checkbox);
});
// Date range should be hidden
expect(screen.queryByText('Backfill Date Range')).not.toBeInTheDocument();
});
it('shows "Today" and "Specific date" options for end date', async () => {
await setupBackfillTest();
await waitFor(() => {
expect(screen.getByText('Backfill Date Range')).toBeInTheDocument();
});
// Find the select for backfill "To" field
// There are multiple selects, we need the one with 'today' value
const selects = screen.getAllByRole('combobox');
const toSelect = selects.find(s => s.value === 'today');
expect(toSelect).toBeInTheDocument();
// Change to specific date
await act(async () => {
fireEvent.change(toSelect, { target: { value: 'specific' } });
});
// Should show an additional date input
await waitFor(() => {
// There should be date inputs visible
const dateInputs = document.querySelectorAll('input[type="date"]');
expect(dateInputs.length).toBeGreaterThanOrEqual(2);
});
});
it('hides backfill options for full strategy', async () => {
await setupBackfillTest();
// Switch to full strategy
const fullRadio = screen.getByLabelText('Full');
await act(async () => {
fireEvent.click(fullRadio);
});
// Backfill checkbox should not be visible
expect(
screen.queryByText('Run initial backfill'),
).not.toBeInTheDocument();
});
});
describe('Schedule Configuration', () => {
const setupScheduleTest = async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(
screen.getByText('Configure Materialization'),
).toBeInTheDocument();
});
};
it('shows schedule dropdown with recommended, hourly, and custom options', async () => {
await setupScheduleTest();
expect(screen.getByText('Schedule')).toBeInTheDocument();
// The schedule select should have options - find one with 'auto' value
const selects = screen.getAllByRole('combobox');
const scheduleSelect = selects.find(s => s.value === 'auto');
expect(scheduleSelect).toBeInTheDocument();
});
it('shows custom cron input when custom is selected', async () => {
await setupScheduleTest();
// Find the schedule select (has 'auto' value initially)
const selects = screen.getAllByRole('combobox');
const scheduleSelect = selects.find(s => s.value === 'auto');
await act(async () => {
fireEvent.change(scheduleSelect, { target: { value: 'custom' } });
});
// Custom input should appear
await waitFor(() => {
expect(screen.getByPlaceholderText('0 6 * * *')).toBeInTheDocument();
});
});
});
describe('Lookback Window', () => {
it('shows lookback window input for incremental strategy', async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(screen.getByText('Lookback Window')).toBeInTheDocument();
});
// Should have placeholder
expect(screen.getByPlaceholderText('1 day')).toBeInTheDocument();
// Should have hint text
expect(screen.getByText(/For late-arriving data/)).toBeInTheDocument();
});
it('hides lookback window for full strategy', async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(
screen.getByText('Configure Materialization'),
).toBeInTheDocument();
});
// Switch to full strategy
const fullRadio = screen.getByLabelText('Full');
await act(async () => {
fireEvent.click(fullRadio);
});
// Lookback window should be hidden
expect(screen.queryByText('Lookback Window')).not.toBeInTheDocument();
});
});
describe('Druid Cube Configuration', () => {
it('shows Druid cube checkbox', async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(
screen.getByText('Enable Druid cube materialization'),
).toBeInTheDocument();
});
});
it('shows cube name inputs when Druid is enabled and no cube loaded', async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(screen.getByText('Cube Name')).toBeInTheDocument();
});
// Should show namespace and name inputs
expect(screen.getByPlaceholderText('users.myname')).toBeInTheDocument();
expect(screen.getByPlaceholderText('my_cube')).toBeInTheDocument();
});
it('shows preview of pre-aggregations to combine', async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(
screen.getByText('Pre-aggregations to combine:'),
).toBeInTheDocument();
});
// Should show the pre-agg source (may appear multiple times)
const repairOrdersElements = screen.getAllByText('repair_orders');
expect(repairOrdersElements.length).toBeGreaterThan(0);
});
});
describe('Form Submission', () => {
it('shows correct button text for Druid cube materialization', async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(
screen.getByText('Create Pre-Agg Workflows & Schedule Cube'),
).toBeInTheDocument();
});
});
it('shows Cancel and submit buttons', async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(screen.getByText('Cancel')).toBeInTheDocument();
});
});
it('closes form when Cancel is clicked', async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={vi.fn()}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(
screen.getByText('Configure Materialization'),
).toBeInTheDocument();
});
// Click Cancel
fireEvent.click(screen.getByText('Cancel'));
// Form should be closed
await waitFor(() => {
expect(
screen.queryByText('Configure Materialization'),
).not.toBeInTheDocument();
});
});
it('calls onPlanMaterialization with config when submitted', async () => {
const onFetchNodePartitions = vi.fn().mockResolvedValue({
columns: [{ name: 'date_id', type: 'int' }],
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
});
const onPlanMaterialization = vi.fn().mockResolvedValue({});
renderWithRouter(
<QueryOverviewPanel
{...defaultProps}
onPlanMaterialization={onPlanMaterialization}
onFetchNodePartitions={onFetchNodePartitions}
/>,
);
// Open config form
const configureBtn = screen.getByText('Configure');
await act(async () => {
fireEvent.click(configureBtn);
});
await waitFor(() => {
expect(
screen.getByText('Create Pre-Agg Workflows & Schedule Cube'),
).toBeInTheDocument();
});
// Submit the form
const submitBtn = screen.getByText(
'Create Pre-Agg Workflows & Schedule Cube',
);
await act(async () => {
fireEvent.click(submitBtn);
});
await waitFor(() => {
expect(onPlanMaterialization).toHaveBeenCalled();
});
// Check the config structure
const callArgs = onPlanMaterialization.mock.calls[0][1];
expect(callArgs).toHaveProperty('strategy');
expect(callArgs).toHaveProperty('schedule');
expect(callArgs).toHaveProperty('enableDruidCube', true);
});
});
});
describe('PreAggDetailsPanel', () => {
const mockPreAgg = {
parent_name: 'default.repair_orders',
aggregability: 'FULL',
grain: ['date_id', 'customer_id'],
components: [
{
name: 'sum_revenue',
expression: 'SUM(revenue)',
aggregation: 'SUM',
merge: 'SUM',
},
{
name: 'count_orders',
expression: 'COUNT(*)',
aggregation: 'COUNT',
merge: 'SUM',
},
],
sql: 'SELECT date_id, customer_id, SUM(revenue) FROM orders GROUP BY 1, 2',
};
const mockMetricFormulas = [
{
name: 'default.total_revenue',
short_name: 'total_revenue',
combiner: 'SUM(sum_revenue)',
is_derived: false,
components: ['sum_revenue'],
},
];
const onClose = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it('returns null when no preAgg provided', () => {
const { container } = render(
<PreAggDetailsPanel preAgg={null} onClose={onClose} />,
);
expect(container.firstChild).toBeNull();
});
it('renders pre-aggregation badge', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('Pre-aggregation')).toBeInTheDocument();
});
it('displays source name', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('repair_orders')).toBeInTheDocument();
expect(screen.getByText('default.repair_orders')).toBeInTheDocument();
});
it('displays close button', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByTitle('Close panel')).toBeInTheDocument();
});
it('calls onClose when close button clicked', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
fireEvent.click(screen.getByTitle('Close panel'));
expect(onClose).toHaveBeenCalled();
});
describe('Grain Section', () => {
it('displays grain section', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('Grain (GROUP BY)')).toBeInTheDocument();
});
it('shows grain columns as pills', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('date_id')).toBeInTheDocument();
expect(screen.getByText('customer_id')).toBeInTheDocument();
});
it('shows empty message when no grain', () => {
const noGrainPreAgg = { ...mockPreAgg, grain: [] };
render(
<PreAggDetailsPanel
preAgg={noGrainPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('No grain columns')).toBeInTheDocument();
});
});
describe('Related Metrics Section', () => {
it('displays metrics using this section', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('Metrics Using This')).toBeInTheDocument();
});
it('shows related metrics', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('total_revenue')).toBeInTheDocument();
});
});
describe('Components Table', () => {
it('displays components section', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('Components (2)')).toBeInTheDocument();
});
it('shows component names', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('sum_revenue')).toBeInTheDocument();
expect(screen.getByText('count_orders')).toBeInTheDocument();
});
it('shows component expressions', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('SUM(revenue)')).toBeInTheDocument();
expect(screen.getByText('COUNT(*)')).toBeInTheDocument();
});
it('shows aggregation functions', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getAllByText('SUM').length).toBeGreaterThan(0);
expect(screen.getByText('COUNT')).toBeInTheDocument();
});
});
describe('SQL Section', () => {
it('displays SQL section when sql is present', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('Pre-Aggregation SQL')).toBeInTheDocument();
});
it('shows copy button', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('Copy SQL')).toBeInTheDocument();
});
it('copies SQL when copy button is clicked', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
const copyBtn = screen.getByText('Copy SQL');
fireEvent.click(copyBtn);
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
mockPreAgg.sql,
);
});
it('hides SQL section when sql is not present', () => {
const preAggWithoutSql = { ...mockPreAgg, sql: null };
render(
<PreAggDetailsPanel
preAgg={preAggWithoutSql}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.queryByText('Pre-Aggregation SQL')).not.toBeInTheDocument();
});
});
describe('Pre-aggregation Type Badge', () => {
it('shows Pre-aggregation badge', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('Pre-aggregation')).toBeInTheDocument();
});
it('shows parent name in title', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
expect(screen.getByText('repair_orders')).toBeInTheDocument();
});
});
describe('Merge Function Display', () => {
it('shows merge functions for components', () => {
render(
<PreAggDetailsPanel
preAgg={mockPreAgg}
metricFormulas={mockMetricFormulas}
onClose={onClose}
/>,
);
// Components have merge functions
expect(screen.getAllByText('SUM').length).toBeGreaterThan(0);
});
});
});
describe('MetricDetailsPanel', () => {
const mockMetric = {
name: 'default.avg_repair_price',
short_name: 'avg_repair_price',
combiner: 'SUM(sum_revenue) / SUM(count_orders)',
is_derived: true,
components: ['sum_revenue', 'count_orders'],
};
const mockGrainGroups = [
{
parent_name: 'default.repair_orders',
components: [{ name: 'sum_revenue' }, { name: 'count_orders' }],
},
];
const onClose = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it('returns null when no metric provided', () => {
const { container } = render(
<MetricDetailsPanel metric={null} onClose={onClose} />,
);
expect(container.firstChild).toBeNull();
});
it('renders metric badge', () => {
render(
<MetricDetailsPanel
metric={mockMetric}
grainGroups={mockGrainGroups}
onClose={onClose}
/>,
);
expect(screen.getByText('Derived Metric')).toBeInTheDocument();
});
it('renders regular metric badge for non-derived', () => {
const nonDerivedMetric = { ...mockMetric, is_derived: false };
render(
<MetricDetailsPanel
metric={nonDerivedMetric}
grainGroups={mockGrainGroups}
onClose={onClose}
/>,
);
expect(screen.getByText('Metric')).toBeInTheDocument();
});
it('displays metric name', () => {
render(
<MetricDetailsPanel
metric={mockMetric}
grainGroups={mockGrainGroups}
onClose={onClose}
/>,
);
expect(screen.getByText('avg_repair_price')).toBeInTheDocument();
expect(screen.getByText('default.avg_repair_price')).toBeInTheDocument();
});
it('calls onClose when close button clicked', () => {
render(
<MetricDetailsPanel
metric={mockMetric}
grainGroups={mockGrainGroups}
onClose={onClose}
/>,
);
fireEvent.click(screen.getByTitle('Close panel'));
expect(onClose).toHaveBeenCalled();
});
describe('Formula Section', () => {
it('displays combiner formula section', () => {
render(
<MetricDetailsPanel
metric={mockMetric}
grainGroups={mockGrainGroups}
onClose={onClose}
/>,
);
expect(screen.getByText('Combiner Formula')).toBeInTheDocument();
});
it('shows the formula', () => {
render(
<MetricDetailsPanel
metric={mockMetric}
grainGroups={mockGrainGroups}
onClose={onClose}
/>,
);
expect(
screen.getByText('SUM(sum_revenue) / SUM(count_orders)'),
).toBeInTheDocument();
});
});
describe('Components Section', () => {
it('displays components used section', () => {
render(
<MetricDetailsPanel
metric={mockMetric}
grainGroups={mockGrainGroups}
onClose={onClose}
/>,
);
expect(screen.getByText('Components Used')).toBeInTheDocument();
});
it('shows component tags', () => {
render(
<MetricDetailsPanel
metric={mockMetric}
grainGroups={mockGrainGroups}
onClose={onClose}
/>,
);
expect(screen.getByText('sum_revenue')).toBeInTheDocument();
expect(screen.getByText('count_orders')).toBeInTheDocument();
});
});
describe('Source Pre-aggregations Section', () => {
it('displays source pre-aggregations section', () => {
render(
<MetricDetailsPanel
metric={mockMetric}
grainGroups={mockGrainGroups}
onClose={onClose}
/>,
);
expect(screen.getByText('Source Pre-aggregations')).toBeInTheDocument();
});
it('shows related pre-agg sources', () => {
render(
<MetricDetailsPanel
metric={mockMetric}
grainGroups={mockGrainGroups}
onClose={onClose}
/>,
);
expect(screen.getByText('repair_orders')).toBeInTheDocument();
});
it('shows empty message when no sources found', () => {
render(
<MetricDetailsPanel
metric={mockMetric}
grainGroups={[]}
onClose={onClose}
/>,
);
expect(screen.getByText('No source found')).toBeInTheDocument();
});
});
describ