Skip to content

Commit 19bcaed

Browse files
authored
test: Add E2E tests for materialized views (#2406)
## Summary This PR adds the following E2E tests covering materialized views 1. MV indicator shows Accelerated in the chart editor: count() over the MV-backed source shows the green "Accelerated" badge. 2. MV indicator + chart data on a saved dashboard tile: the saved tile renders a recharts chart (data populated from the MV) and shows the accelerated icon. Covers both the dashboard-tile indicator and "charts populated from MV data." 3. MV modal shows correct config: opens the modal from the indicator and verifies the active MV (e2e_otel_traces_1m), "1 minute" granularity, dimension columns (ServiceName/StatusCode), and the aggregated-columns table (count + avg/max/quantile of Duration). 4. Configuring an MV with auto-population: adds an MV in the source form, picks the MV table, and verifies inference fills granularity, dimension columns, timestamp column, and the 4 aggregated-column rows. ### Screenshots or video No behavior changes, just tests and test ids ### References - Linear Issue: Closes HDX-4436 - Related PRs:
1 parent 418567f commit 19bcaed

14 files changed

Lines changed: 633 additions & 80 deletions

File tree

‎docker/clickhouse/local/init-db-e2e.sh‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,5 +209,38 @@ ENGINE = MergeTree
209209
PARTITION BY toDate(TimeUnix)
210210
ORDER BY (ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix))
211211
TTL toDate(TimeUnix) + toIntervalDay(30)
212-
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1
212+
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;
213+
214+
-- Materialized view rollup for traces, used by E2E tests covering MV
215+
-- acceleration. The target table pre-aggregates Duration over 1-minute
216+
-- buckets grouped by the dimension columns ServiceName and StatusCode.
217+
-- The 'E2E Traces MV' fixture source references e2e_otel_traces_1m.
218+
CREATE TABLE IF NOT EXISTS ${DATABASE}.e2e_otel_traces_1m
219+
(
220+
\`Timestamp\` DateTime,
221+
\`ServiceName\` LowCardinality(String),
222+
\`StatusCode\` LowCardinality(String),
223+
\`count\` SimpleAggregateFunction(sum, UInt64),
224+
\`avg__Duration\` AggregateFunction(avg, UInt64),
225+
\`max__Duration\` SimpleAggregateFunction(max, UInt64),
226+
\`quantile__Duration\` AggregateFunction(quantile(0.95), UInt64)
227+
)
228+
ENGINE = AggregatingMergeTree
229+
ORDER BY (Timestamp, ServiceName, StatusCode)
230+
SETTINGS index_granularity = 8192;
231+
232+
CREATE MATERIALIZED VIEW IF NOT EXISTS ${DATABASE}.e2e_otel_traces_1m_mv TO ${DATABASE}.e2e_otel_traces_1m
233+
AS SELECT
234+
toStartOfMinute(Timestamp) AS Timestamp,
235+
ServiceName,
236+
StatusCode,
237+
count() AS count,
238+
avgState(Duration) AS avg__Duration,
239+
maxSimpleState(Duration) AS max__Duration,
240+
quantileState(0.95)(Duration) AS quantile__Duration
241+
FROM ${DATABASE}.e2e_otel_traces
242+
GROUP BY
243+
Timestamp,
244+
ServiceName,
245+
StatusCode
213246
EOFSQL

‎packages/app/src/components/MaterializedViews/MVOptimizationIndicator.tsx‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,23 @@ function MVOptimizationIcon({
2323
}) {
2424
return isInWarningState ? (
2525
<Tooltip label="Not Accelerated">
26-
<ActionIcon onClick={onClick}>
26+
<ActionIcon
27+
onClick={onClick}
28+
aria-label="Not Accelerated"
29+
data-testid="mv-optimization-indicator"
30+
data-mv-accelerated="false"
31+
>
2732
<IconBoltOff size={16} color={WARNING_COLOR} />
2833
</ActionIcon>
2934
</Tooltip>
3035
) : (
3136
<Tooltip label="Accelerated">
32-
<ActionIcon onClick={onClick}>
37+
<ActionIcon
38+
onClick={onClick}
39+
aria-label="Accelerated"
40+
data-testid="mv-optimization-indicator"
41+
data-mv-accelerated="true"
42+
>
3343
<IconBolt size={18} color={SUCCESS_COLOR} />
3444
</ActionIcon>
3545
</Tooltip>
@@ -48,6 +58,8 @@ function MVOptimizationBadge({
4858
color={isInWarningState ? WARNING_COLOR : SUCCESS_COLOR}
4959
onClick={onClick}
5060
className="cursor-pointer"
61+
data-testid="mv-optimization-indicator"
62+
data-mv-accelerated={isInWarningState ? 'false' : 'true'}
5163
>
5264
{isInWarningState ? 'Not Accelerated' : 'Accelerated'}
5365
</Badge>

‎packages/app/src/components/MaterializedViews/MVOptimizationModal.tsx‎

Lines changed: 81 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -47,69 +47,90 @@ export default function MVOptimizationModal({
4747
onClose={onClose}
4848
size="lg"
4949
>
50-
<Text size="sm" mb="sm">
51-
This source is configured with{' '}
52-
{hasMultipleMVs ? 'materialized views' : 'a materialized view'} for
53-
accelerating some aggregations.
54-
</Text>
50+
<div data-testid="mv-optimization-modal">
51+
<Text size="sm" mb="sm">
52+
This source is configured with{' '}
53+
{hasMultipleMVs ? 'materialized views' : 'a materialized view'} for
54+
accelerating some aggregations.
55+
</Text>
5556

56-
<Accordion defaultValue={firstUsedMv && mvConfigToKey(firstUsedMv)}>
57-
{mvConfigs.map(config => {
58-
const key = mvConfigToKey(config);
59-
const explanation = explanationsByKey.get(key);
60-
const hasErrors = !!explanation?.errors.length;
61-
const isBeingUsedByOptimizedConfig = explanation?.success;
62-
const rowEstimate =
63-
explanation?.rowEstimate?.toLocaleString() ?? 'N/A';
57+
<Accordion defaultValue={firstUsedMv && mvConfigToKey(firstUsedMv)}>
58+
{mvConfigs.map(config => {
59+
const key = mvConfigToKey(config);
60+
const explanation = explanationsByKey.get(key);
61+
const hasErrors = !!explanation?.errors.length;
62+
const isBeingUsedByOptimizedConfig = explanation?.success;
63+
const rowEstimate =
64+
explanation?.rowEstimate?.toLocaleString() ?? 'N/A';
6465

65-
return (
66-
<Accordion.Item value={key} key={key}>
67-
<Accordion.Control px="xs">
68-
<Group justify="space-between">
69-
<Text>{config.tableName}</Text>
70-
{isBeingUsedByOptimizedConfig ? (
71-
<Tooltip label={`Estimated rows scanned: ${rowEstimate}`}>
72-
<Badge me="md" color={SUCCESS_COLOR}>
73-
Active
74-
</Badge>
75-
</Tooltip>
76-
) : hasErrors ? (
77-
<Tooltip label="This materialized view is not compatible with the selected query.">
78-
<Badge me="md" color={WARNING_COLOR}>
79-
Incompatible
80-
</Badge>
81-
</Tooltip>
82-
) : explanation ? (
83-
<Tooltip label={`Estimated rows scanned: ${rowEstimate}`}>
84-
<Badge me="md" color="gray">
85-
Skipped
86-
</Badge>
87-
</Tooltip>
88-
) : null}
89-
</Group>
90-
</Accordion.Control>
91-
<Accordion.Panel>
92-
<>
93-
<MVConfigSummary config={config} />
94-
{hasErrors && (
95-
<Alert color="red" mt="xs">
96-
<Text size="sm" fw={500} mb="xs">
97-
The query cannot be accelerated using this materialized
98-
view for the following reason(s):
99-
</Text>
100-
{explanation.errors.map((error, idx) => (
101-
<Text size="sm" key={idx} mt="xs">
102-
{error}
66+
return (
67+
<Accordion.Item value={key} key={key}>
68+
<Accordion.Control
69+
px="xs"
70+
data-testid="mv-optimization-modal-item"
71+
data-mv-table={config.tableName}
72+
>
73+
<Group justify="space-between">
74+
<Text>{config.tableName}</Text>
75+
{isBeingUsedByOptimizedConfig ? (
76+
<Tooltip label={`Estimated rows scanned: ${rowEstimate}`}>
77+
<Badge
78+
me="md"
79+
color={SUCCESS_COLOR}
80+
data-testid="mv-optimization-modal-status"
81+
data-mv-status="active"
82+
>
83+
Active
84+
</Badge>
85+
</Tooltip>
86+
) : hasErrors ? (
87+
<Tooltip label="This materialized view is not compatible with the selected query.">
88+
<Badge
89+
me="md"
90+
color={WARNING_COLOR}
91+
data-testid="mv-optimization-modal-status"
92+
data-mv-status="incompatible"
93+
>
94+
Incompatible
95+
</Badge>
96+
</Tooltip>
97+
) : explanation ? (
98+
<Tooltip label={`Estimated rows scanned: ${rowEstimate}`}>
99+
<Badge
100+
me="md"
101+
color="gray"
102+
data-testid="mv-optimization-modal-status"
103+
data-mv-status="skipped"
104+
>
105+
Skipped
106+
</Badge>
107+
</Tooltip>
108+
) : null}
109+
</Group>
110+
</Accordion.Control>
111+
<Accordion.Panel>
112+
<>
113+
<MVConfigSummary config={config} />
114+
{hasErrors && (
115+
<Alert color="red" mt="xs">
116+
<Text size="sm" fw={500} mb="xs">
117+
The query cannot be accelerated using this
118+
materialized view for the following reason(s):
103119
</Text>
104-
))}
105-
</Alert>
106-
)}
107-
</>
108-
</Accordion.Panel>
109-
</Accordion.Item>
110-
);
111-
})}
112-
</Accordion>
120+
{explanation.errors.map((error, idx) => (
121+
<Text size="sm" key={idx} mt="xs">
122+
{error}
123+
</Text>
124+
))}
125+
</Alert>
126+
)}
127+
</>
128+
</Accordion.Panel>
129+
</Accordion.Item>
130+
);
131+
})}
132+
</Accordion>
133+
</div>
113134
</Modal>
114135
);
115136
}

‎packages/app/src/components/Sources/SourceForm.tsx‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ function MaterializedViewsFormSection({ control, setValue }: TableModelProps) {
550550

551551
<Button
552552
variant="secondary"
553+
data-testid="add-materialized-view-button"
553554
onClick={() => {
554555
appendMaterializedView({
555556
databaseName: databaseName,
@@ -688,7 +689,7 @@ function MaterializedViewFormSection({
688689
});
689690

690691
return (
691-
<Stack gap="sm">
692+
<Stack gap="sm" data-testid="mv-form-section" data-mv-index={mvIndex}>
692693
<Grid columns={2} flex={1}>
693694
<Grid.Col span={1}>
694695
<DatabaseSelectControlled
@@ -699,7 +700,7 @@ function MaterializedViewFormSection({
699700
</Grid.Col>
700701
<Grid.Col span={1}>
701702
<Group>
702-
<Box flex={1}>
703+
<Box flex={1} data-testid="mv-table-select">
703704
<DBTableSelectControlled
704705
database={mvDatabaseName}
705706
control={control}
@@ -713,7 +714,7 @@ function MaterializedViewFormSection({
713714
</Group>
714715
</Grid.Col>
715716

716-
<Grid.Col span={2}>
717+
<Grid.Col span={2} data-testid="mv-timestamp-column">
717718
<Text size="xs" fw={500} mb={4}>
718719
Timestamp Column
719720
</Text>
@@ -730,7 +731,7 @@ function MaterializedViewFormSection({
730731
/>
731732
</Grid.Col>
732733

733-
<Grid.Col span={1}>
734+
<Grid.Col span={1} data-testid="mv-granularity-select">
734735
<Text size="xs" fw={500} mb={4}>
735736
Granularity
736737
<Tooltip
@@ -792,7 +793,7 @@ function MaterializedViewFormSection({
792793
</Grid.Col>
793794
</Grid>
794795

795-
<Box>
796+
<Box data-testid="mv-dimension-columns">
796797
<Text size="xs" fw={500} mb={4}>
797798
Dimension Columns (comma-separated)
798799
<Tooltip
@@ -943,7 +944,7 @@ function AggregatedColumnsFormSection({
943944
<IconHelpCircle size={14} className="cursor-pointer ms-1" />
944945
</Tooltip>
945946
</Text>
946-
<Grid columns={10}>
947+
<Grid columns={10} data-testid="mv-aggregated-columns">
947948
{aggregates.map((field, colIndex) => (
948949
<AggregatedColumnRow
949950
key={field.id}
@@ -955,7 +956,13 @@ function AggregatedColumnsFormSection({
955956
/>
956957
))}
957958
</Grid>
958-
<Button size="sm" variant="secondary" onClick={addAggregate} mt="lg">
959+
<Button
960+
size="sm"
961+
variant="secondary"
962+
onClick={addAggregate}
963+
mt="lg"
964+
data-testid="add-aggregated-column-button"
965+
>
959966
<Group>
960967
<IconCirclePlus size={16} />
961968
Add Column
@@ -1000,7 +1007,11 @@ function AggregatedColumnRow({
10001007

10011008
return (
10021009
<>
1003-
<Grid.Col span={2}>
1010+
<Grid.Col
1011+
span={2}
1012+
data-testid="mv-aggregated-column-fn"
1013+
data-col-index={colIndex}
1014+
>
10041015
<SelectControlled
10051016
control={control}
10061017
name={`materializedViews.${mvIndex}.aggregatedColumns.${colIndex}.aggFn`}

‎packages/app/tests/e2e/components/ChartEditorComponent.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,13 @@ export class ChartEditorComponent {
6969
*/
7070
async selectSource(sourceName: string) {
7171
await this.sourceSelector.click();
72-
// Use getByRole for more reliable selection
73-
const sourceOption = this.page.getByRole('option', { name: sourceName });
72+
// Use getByRole for more reliable selection. exact: true avoids matching
73+
// sources whose names are prefixes of others (e.g. "E2E Traces MV" vs
74+
// "E2E Traces MV AutoPopulate").
75+
const sourceOption = this.page.getByRole('option', {
76+
name: sourceName,
77+
exact: true,
78+
});
7479
if ((await sourceOption.getAttribute('data-combobox-active')) != 'true') {
7580
await sourceOption.click({ timeout: 5000 });
7681
}

0 commit comments

Comments
 (0)