chore: Add Cypress tests for drill by (#23849)

This commit is contained in:
Kamil Gabryjelski 2023-04-28 17:33:45 +02:00 committed by GitHub
parent 31d33592ef
commit 60046ca1cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 746 additions and 16 deletions

View File

@ -0,0 +1,705 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// eslint-disable-next-line import/no-extraneous-dependencies
import { Interception } from 'cypress/types/net-stubbing';
import { waitForChartLoad } from 'cypress/utils';
import { SUPPORTED_CHARTS_DASHBOARD } from 'cypress/utils/urls';
import {
openTopLevelTab,
SUPPORTED_TIER1_CHARTS,
SUPPORTED_TIER2_CHARTS,
} from './utils';
import {
interceptExploreJson,
interceptV1ChartData,
interceptFormDataKey,
} from '../explore/utils';
const closeModal = () => {
cy.get('body').then($body => {
if ($body.find('[data-test="close-drill-by-modal"]').length) {
cy.getBySel('close-drill-by-modal').click({ force: true });
}
});
};
const openTableContextMenu = (
cellContent: string,
tableSelector = "[data-test-viz-type='table']",
) => {
cy.get(tableSelector)
.scrollIntoView()
.contains(cellContent)
.first()
.rightclick();
};
const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
if (isLegacy) {
interceptExploreJson('legacyData');
} else {
interceptV1ChartData();
}
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)')
.first()
.find("[role='menu'] [role='menuitem'] [title='Drill by']")
.trigger('mouseover');
cy.get(
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-hidden) [data-test="drill-by-submenu"]',
)
.find('[role="menuitem"]')
.contains(new RegExp(`^${targetDrillByColumn}$`))
.first()
.click({ force: true });
if (isLegacy) {
return cy.wait('@legacyData');
}
return cy.wait('@v1Data');
};
const verifyExpectedFormData = (
interceptedRequest: Interception,
expectedFormData: Record<string, any>,
) => {
const actualFormData = interceptedRequest.request.body?.form_data;
Object.entries(expectedFormData).forEach(([key, val]) => {
expect(actualFormData?.[key]).to.eql(val);
});
};
const testEchart = (
vizType: string,
chartName: string,
drillClickCoordinates: [[number, number], [number, number]],
furtherDrillDimension = 'name',
) => {
cy.get(`[data-test-viz-type='${vizType}'] canvas`).then($canvas => {
// click 'boy'
cy.wrap($canvas)
.scrollIntoView()
.trigger(
'mouseover',
drillClickCoordinates[0][0],
drillClickCoordinates[0][1],
)
.rightclick(drillClickCoordinates[0][0], drillClickCoordinates[0][1]);
drillBy('state').then(intercepted => {
verifyExpectedFormData(intercepted, {
groupby: ['state'],
adhoc_filters: [
{
clause: 'WHERE',
comparator: 'boy',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'gender',
},
],
});
});
cy.getBySel(`"Drill by: ${chartName}-modal"`).as('drillByModal');
cy.get('@drillByModal')
.find('.draggable-trigger')
.should('contain', chartName);
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('contain', 'state');
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible');
// further drill
cy.get(`[data-test="drill-by-chart"] canvas`).then($canvas => {
// click 'other'
cy.wrap($canvas)
.scrollIntoView()
.trigger(
'mouseover',
drillClickCoordinates[1][0],
drillClickCoordinates[1][1],
)
.rightclick(drillClickCoordinates[1][0], drillClickCoordinates[1][1]);
drillBy(furtherDrillDimension).then(intercepted => {
verifyExpectedFormData(intercepted, {
groupby: [furtherDrillDimension],
adhoc_filters: [
{
clause: 'WHERE',
comparator: 'boy',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'gender',
},
{
clause: 'WHERE',
comparator: 'other',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'state',
},
],
});
});
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible');
// undo - back to drill by state
interceptV1ChartData('drillByUndo');
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('contain', 'state (other)')
.and('contain', furtherDrillDimension)
.contains('state (other)')
.click();
cy.wait('@drillByUndo').then(intercepted => {
verifyExpectedFormData(intercepted, {
groupby: ['state'],
adhoc_filters: [
{
clause: 'WHERE',
comparator: 'boy',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'gender',
},
],
});
});
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('not.contain', 'state (other)')
.and('not.contain', furtherDrillDimension)
.and('contain', 'state');
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible');
});
});
};
describe('Drill by modal', () => {
beforeEach(() => {
closeModal();
});
before(() => {
cy.visit(SUPPORTED_CHARTS_DASHBOARD);
});
describe('Modal actions + Table', () => {
before(() => {
closeModal();
openTopLevelTab('Tier 1');
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
});
it('opens the modal from the context menu', () => {
openTableContextMenu('boy');
drillBy('state').then(intercepted => {
verifyExpectedFormData(intercepted, {
groupby: ['state'],
adhoc_filters: [
{
clause: 'WHERE',
comparator: 'boy',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'gender',
},
],
});
});
cy.getBySel('"Drill by: Table-modal"').as('drillByModal');
cy.get('@drillByModal')
.find('.draggable-trigger')
.should('contain', 'Drill by: Table');
cy.get('@drillByModal')
.find('[data-test="metadata-bar"]')
.should('be.visible');
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('contain', 'state');
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible')
.and('contain', 'state')
.and('contain', 'sum__num');
// further drilling
openTableContextMenu('CA', '[data-test="drill-by-chart"]');
drillBy('name').then(intercepted => {
verifyExpectedFormData(intercepted, {
groupby: ['name'],
adhoc_filters: [
{
clause: 'WHERE',
comparator: 'boy',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'gender',
},
{
clause: 'WHERE',
comparator: 'CA',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'state',
},
],
});
});
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible')
.and('not.contain', 'state')
.and('contain', 'name')
.and('contain', 'sum__num');
// undo - back to drill by state
interceptV1ChartData('drillByUndo');
interceptFormDataKey();
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('contain', 'state (CA)')
.and('contain', 'name')
.contains('state (CA)')
.click();
cy.wait('@drillByUndo').then(intercepted => {
verifyExpectedFormData(intercepted, {
groupby: ['state'],
adhoc_filters: [
{
clause: 'WHERE',
comparator: 'boy',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'gender',
},
],
});
});
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible')
.and('not.contain', 'name')
.and('contain', 'state')
.and('contain', 'sum__num');
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('not.contain', 'state (CA)')
.and('not.contain', 'name')
.and('contain', 'state');
cy.get('@drillByModal')
.find('[data-test="drill-by-display-toggle"]')
.contains('Table')
.click();
cy.getBySel('drill-by-chart').should('not.exist');
cy.get('@drillByModal')
.find('[data-test="drill-by-results-table"]')
.should('be.visible');
cy.wait('@formDataKey').then(intercept => {
cy.get('@drillByModal')
.contains('Edit chart')
.should('have.attr', 'href')
.and(
'contain',
`/explore/?form_data_key=${intercept.response?.body?.key}`,
);
});
});
});
describe('Tier 1 charts', () => {
before(() => {
closeModal();
openTopLevelTab('Tier 1');
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
});
it('Pivot Table', () => {
openTableContextMenu('boy', "[data-test-viz-type='pivot_table_v2']");
drillBy('name').then(intercepted => {
verifyExpectedFormData(intercepted, {
groupbyRows: ['state'],
groupbyColumns: ['name'],
adhoc_filters: [
{
clause: 'WHERE',
comparator: 'boy',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'gender',
},
],
});
});
cy.getBySel('"Drill by: Pivot Table-modal"').as('drillByModal');
cy.get('@drillByModal')
.find('.draggable-trigger')
.should('contain', 'Drill by: Pivot Table');
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('contain', 'name');
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible')
.and('contain', 'state')
.and('contain', 'name')
.and('contain', 'sum__num')
.and('not.contain', 'Gender');
openTableContextMenu('CA', '[data-test="drill-by-chart"]');
drillBy('ds').then(intercepted => {
verifyExpectedFormData(intercepted, {
groupbyColumns: ['name'],
groupbyRows: ['ds'],
adhoc_filters: [
{
clause: 'WHERE',
comparator: 'boy',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'gender',
},
{
clause: 'WHERE',
comparator: 'CA',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'state',
},
],
});
});
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible')
.and('contain', 'name')
.and('contain', 'ds')
.and('contain', 'sum__num')
.and('not.contain', 'state');
interceptV1ChartData('drillByUndo');
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('contain', 'name (CA)')
.and('contain', 'ds')
.contains('name (CA)')
.click();
cy.wait('@drillByUndo').then(intercepted => {
verifyExpectedFormData(intercepted, {
groupbyRows: ['state'],
groupbyColumns: ['name'],
adhoc_filters: [
{
clause: 'WHERE',
comparator: 'boy',
expressionType: 'SIMPLE',
operator: '==',
operatorId: 'EQUALS',
subject: 'gender',
},
],
});
});
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible')
.and('not.contain', 'ds')
.and('contain', 'state')
.and('contain', 'name')
.and('contain', 'sum__num');
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('not.contain', 'name (CA)')
.and('not.contain', 'ds')
.and('contain', 'name');
});
it('Line chart', () => {
testEchart('echarts_timeseries_line', 'Time-Series Line Chart', [
[70, 93],
[70, 93],
]);
});
it('Area Chart', () => {
testEchart('echarts_area', 'Time-Series Area Chart', [
[70, 93],
[70, 93],
]);
});
it('Time-Series Scatter Chart', () => {
testEchart('echarts_timeseries_scatter', 'Time-Series Scatter Chart', [
[70, 93],
[70, 93],
]);
});
it('Time-Series Bar Chart V2', () => {
testEchart('echarts_timeseries_bar', 'Time-Series Bar Chart V2', [
[70, 94],
[362, 68],
]);
});
it('Pie Chart', () => {
testEchart('pie', 'Pie Chart', [
[243, 167],
[534, 248],
]);
});
});
describe('Tier 2 charts', () => {
before(() => {
closeModal();
openTopLevelTab('Tier 2');
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
});
it('Box Plot Chart', () => {
testEchart(
'box_plot',
'Box Plot Chart',
[
[139, 277],
[787, 441],
],
'ds',
);
});
it('Time-Series Generic Chart', () => {
testEchart('echarts_timeseries', 'Time-Series Generic Chart', [
[70, 93],
[70, 93],
]);
});
it('Time-Series Smooth Line Chart', () => {
testEchart('echarts_timeseries_smooth', 'Time-Series Smooth Line Chart', [
[70, 93],
[70, 93],
]);
});
it('Time-Series Step Line Chart', () => {
testEchart('echarts_timeseries_step', 'Time-Series Step Line Chart', [
[70, 93],
[70, 93],
]);
});
it('Funnel Chart', () => {
testEchart('funnel', 'Funnel Chart', [
[154, 80],
[421, 39],
]);
});
it('Gauge Chart', () => {
testEchart('gauge_chart', 'Gauge Chart', [
[151, 95],
[300, 143],
]);
});
it('Radar Chart', () => {
testEchart('radar', 'Radar Chart', [
[182, 49],
[423, 91],
]);
});
it('Treemap V2 Chart', () => {
testEchart('treemap_v2', 'Treemap V2 Chart', [
[145, 84],
[220, 105],
]);
});
it('Mixed Chart', () => {
cy.get('[data-test-viz-type="mixed_timeseries"] canvas').then($canvas => {
// click 'boy'
cy.wrap($canvas)
.scrollIntoView()
.trigger('mouseover', 70, 93)
.rightclick(70, 93);
drillBy('name').then(intercepted => {
const { queries } = intercepted.request.body;
expect(queries[0].columns).to.eql(['name']);
expect(queries[0].filters).to.eql([
{ col: 'gender', op: '==', val: 'boy' },
]);
expect(queries[1].columns).to.eql(['state']);
expect(queries[1].filters).to.eql([]);
});
cy.getBySel('"Drill by: Mixed Chart-modal"').as('drillByModal');
cy.get('@drillByModal')
.find('.draggable-trigger')
.should('contain', 'Mixed Chart');
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('contain', 'name');
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible');
// further drill
cy.get(`[data-test="drill-by-chart"] canvas`).then($canvas => {
// click second query
cy.wrap($canvas)
.scrollIntoView()
.trigger('mouseover', 246, 114)
.rightclick(246, 114);
drillBy('ds').then(intercepted => {
const { queries } = intercepted.request.body;
expect(queries[0].columns).to.eql(['name']);
expect(queries[0].filters).to.eql([
{ col: 'gender', op: '==', val: 'boy' },
]);
expect(queries[1].columns).to.eql(['ds']);
expect(queries[1].filters).to.eql([
{ col: 'state', op: '==', val: 'other' },
]);
});
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible');
// undo - back to drill by state
interceptV1ChartData('drillByUndo');
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('contain', 'name (other)')
.and('contain', 'ds')
.contains('name (other)')
.click();
cy.wait('@drillByUndo').then(intercepted => {
const { queries } = intercepted.request.body;
expect(queries[0].columns).to.eql(['name']);
expect(queries[0].filters).to.eql([
{ col: 'gender', op: '==', val: 'boy' },
]);
expect(queries[1].columns).to.eql(['state']);
expect(queries[1].filters).to.eql([]);
});
cy.get('@drillByModal')
.find('.ant-breadcrumb')
.should('be.visible')
.and('contain', 'gender (boy)')
.and('contain', '/')
.and('not.contain', 'name (other)')
.and('not.contain', 'ds')
.and('contain', 'name');
cy.get('@drillByModal')
.find('[data-test="drill-by-chart"]')
.should('be.visible');
});
});
});
});
});

View File

@ -18,7 +18,11 @@
*/
import { waitForChartLoad } from 'cypress/utils';
import { SUPPORTED_CHARTS_DASHBOARD } from 'cypress/utils/urls';
import { SUPPORTED_TIER1_CHARTS, SUPPORTED_TIER2_CHARTS } from './utils';
import {
openTopLevelTab,
SUPPORTED_TIER1_CHARTS,
SUPPORTED_TIER2_CHARTS,
} from './utils';
function interceptSamples() {
cy.intercept(`/datasource/samples*`).as('samples');
@ -77,10 +81,6 @@ function closeModal() {
});
}
function setTopLevelTab(tabName: string) {
cy.get("div#TABS-TOP div[role='tab']").contains(tabName).click();
}
function testTimeChart(vizType: string) {
interceptSamples();
@ -139,7 +139,7 @@ describe('Drill to detail modal', () => {
describe('Tier 1 charts', () => {
before(() => {
cy.visit(SUPPORTED_CHARTS_DASHBOARD);
setTopLevelTab('Tier 1');
openTopLevelTab('Tier 1');
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
});
@ -438,7 +438,7 @@ describe('Drill to detail modal', () => {
describe('Tier 2 charts', () => {
before(() => {
cy.visit(SUPPORTED_CHARTS_DASHBOARD);
setTopLevelTab('Tier 2');
openTopLevelTab('Tier 2');
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
});

View File

@ -37,10 +37,24 @@ export const SUPPORTED_TIER1_CHARTS = [
{ name: 'Big Number', viz: 'big_number_total' },
{ name: 'Big Number with Trendline', viz: 'big_number' },
{ name: 'Pie Chart', viz: 'pie' },
{ name: 'Table', viz: 'table' },
{ name: 'Pivot Table', viz: 'pivot_table_v2' },
{ name: 'Time-Series Line Chart', viz: 'echarts_timeseries_line' },
{ name: 'Time-Series Area Chart', viz: 'echarts_area' },
{ name: 'Time-Series Scatter Chart', viz: 'echarts_timeseries_scatter' },
{ name: 'Time-Series Bar Chart V2', viz: 'echarts_timeseries_bar' },
] as ChartSpec[];
export const SUPPORTED_TIER2_CHARTS = [
{ name: 'Box Plot Chart', viz: 'box_plot' },
{ name: 'Time-Series Generic Chart', viz: 'echarts_timeseries' },
{ name: 'Time-Series Smooth Line Chart', viz: 'echarts_timeseries_smooth' },
{ name: 'Time-Series Step Line Chart', viz: 'echarts_timeseries_step' },
{ name: 'Funnel Chart', viz: 'funnel' },
{ name: 'Gauge Chart', viz: 'gauge_chart' },
{ name: 'Radar Chart', viz: 'radar' },
{ name: 'Treemap V2 Chart', viz: 'treemap_v2' },
{ name: 'Mixed Chart', viz: 'mixed_timeseries' },
] as ChartSpec[];
export const testItems = {
@ -515,3 +529,7 @@ export function openTab(tabComponentIndex: number, tabIndex: number) {
.eq(tabIndex)
.click();
}
export const openTopLevelTab = (tabName: string) => {
cy.get("div#TABS-TOP div[role='tab']").contains(tabName).click();
};

View File

@ -35,13 +35,17 @@ export function interceptUpdate() {
cy.intercept('PUT', `/api/v1/chart/*`).as('update');
}
export function interceptPost() {
cy.intercept('POST', `/api/v1/chart/`).as('post');
export const interceptV1ChartData = (alias = 'v1Data') => {
cy.intercept('/api/v1/chart/data*').as(alias);
};
export function interceptExploreJson(alias = 'getJson') {
cy.intercept('POST', `/superset/explore_json/**`).as(alias);
}
export function interceptExploreJson() {
cy.intercept('POST', `/superset/explore_json/**`).as('getJson');
}
export const interceptFormDataKey = () => {
cy.intercept('POST', '/api/v1/explore/form_data').as('formDataKey');
};
export function interceptExploreGet() {
cy.intercept({

View File

@ -54,6 +54,7 @@ export default function DrillByChart({
height: 100%;
min-height: 0;
`}
data-test="drill-by-chart"
>
<SuperChart
disableErrorBoundary

View File

@ -36,6 +36,7 @@ export const useDisplayModeToggle = () => {
box-shadow: none;
}
`}
data-test="drill-by-display-toggle"
>
<Radio.Group
onChange={({ target: { value } }) => {

View File

@ -40,7 +40,7 @@ export const useResultsTableView = (
}
if (chartDataResult.length === 1) {
return (
<PaginationContainer>
<PaginationContainer data-test="drill-by-results-table">
<SingleQueryResultPane
colnames={chartDataResult[0].colnames}
coltypes={chartDataResult[0].coltypes}
@ -53,7 +53,7 @@ export const useResultsTableView = (
);
}
return (
<Tabs fullWidth={false}>
<Tabs fullWidth={false} data-test="drill-by-results-tabs">
{chartDataResult.map((res, index) => (
<Tabs.TabPane tab={t('Results %s', index + 1)} key={index}>
<PaginationContainer>

View File

@ -335,7 +335,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
viz_type="mixed_timeseries",
metrics=["sum__num"],
groupby=["gender"],
metrics_b=["count"],
metrics_b=["sum__num"],
groupby_b=["state"],
),
),

View File

@ -61,7 +61,7 @@ PRESTO_POLL_INTERVAL = 0.1
HIVE_POLL_INTERVAL = 0.1
SQL_MAX_ROW = 10000
SQLLAB_CTAS_NO_LIMIT = True # SQL_MAX_ROW will not take affect for the CTA queries
SQLLAB_CTAS_NO_LIMIT = True # SQL_MAX_ROW will not take effect for the CTA queries
FEATURE_FLAGS = {
**FEATURE_FLAGS,
"foo": "bar",
@ -71,6 +71,7 @@ FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"DASHBOARD_NATIVE_FILTERS": True,
"DRILL_TO_DETAIL": True,
"DRILL_BY": True,
"HORIZONTAL_FILTER_BAR": True,
}