chore: E2E tests for the Drill to detail modal (#21187)

* Add example ECharts Dashboard

* [WIP] E2E test

* Add echarts interactions

* Lint

* DRY
This commit is contained in:
Geido 2022-08-29 11:25:27 +03:00 committed by GitHub
parent ccb293a083
commit f017f98b88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 548 additions and 7 deletions

View File

@ -23,6 +23,7 @@ export const WORLD_HEALTH_DASHBOARD = '/superset/dashboard/world_health/';
export const USA_BIRTH_NAMES_DASHBOARD = '/superset/dashboard/births/';
export const testDashboard = '/superset/dashboard/538/';
export const TABBED_DASHBOARD = '/superset/dashboard/tabbed_dash/';
export const ECHARTS_DASHBOARD = '/superset/dashboard/echarts_dash/';
export const testItems = {
dashboard: 'Cypress test Dashboard',
@ -73,6 +74,14 @@ export const WORLD_HEALTH_CHARTS = [
{ name: 'Box plot', viz: 'box_plot' },
] as const;
export const ECHARTS_CHARTS = [
{ name: 'Number of Girls', viz: 'big_number_total' },
{ name: 'Participants', viz: 'big_number' },
{ name: 'Box plot', viz: 'box_plot' },
{ name: 'Genders', viz: 'pie' },
{ name: 'Energy Force Layout', viz: 'graph_chart' },
] as const;
/** Used to specify charts expected by the test suite */
export interface ChartSpec {
name: string;
@ -81,7 +90,7 @@ export interface ChartSpec {
export function getChartGridComponent({ name, viz }: ChartSpec) {
return cy
.get(`[data-test="chart-grid-component"][data-test-chart-name="${name}"]`)
.get(`[data-test-chart-name="${name}"]`)
.should('have.attr', 'data-test-viz-type', viz);
}
@ -92,7 +101,7 @@ export function waitForChartLoad(chart: ChartSpec) {
return (
cy
// this id only becomes visible when the chart is loaded
.get(`[data-test="chart-grid-component"] #chart-id-${chartId}`, {
.get(`#chart-id-${chartId}`, {
timeout: 30000,
})
.should('be.visible')

View File

@ -0,0 +1,271 @@
/**
* 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.
*/
import {
waitForChartLoad,
ECHARTS_CHARTS,
ECHARTS_DASHBOARD,
} from './dashboard.helper';
function interceptSamples() {
cy.intercept(`/datasource/samples*`).as('samples');
}
function openModalFromMenu(chartType: string) {
interceptSamples();
cy.get(
`[data-test-viz-type='${chartType}'] [aria-label='More Options']`,
).click();
cy.get('.ant-dropdown')
.not('.ant-dropdown-hidden')
.find("[role='menu'] [role='menuitem']")
.eq(5)
.should('contain', 'Drill to detail')
.click();
cy.wait('@samples');
}
function openModalFromChartContext(targetMenuItem: string) {
interceptSamples();
cy.get('.ant-dropdown')
.not('.ant-dropdown-hidden')
.find("[role='menu'] [role='menuitem']")
.should('contain', targetMenuItem)
.click();
cy.wait('@samples');
}
describe('Drill to detail modal', () => {
beforeEach(() => {
cy.login();
cy.visit(ECHARTS_DASHBOARD);
ECHARTS_CHARTS.forEach(waitForChartLoad);
});
it('opens the modal from the context menu', () => {
openModalFromMenu('big_number_total');
cy.get("[role='dialog'] .draggable-trigger").should(
'contain',
'Drill to detail: Number of Girls',
);
});
it('refreshes the data', () => {
openModalFromMenu('big_number_total');
// move to the last page
cy.get(".pagination-container [role='navigation'] [role='button']")
.eq(7)
.click();
cy.wait('@samples');
// reload
cy.get("[aria-label='reload']").click();
cy.wait('@samples');
// make sure it started back from first page
cy.get(".pagination-container [role='navigation'] li.active").should(
'contain',
'1',
);
});
it('paginates', () => {
openModalFromMenu('big_number_total');
// checking the data
cy.get("[data-test='row-count-label']").should('contain', '36.4k rows');
cy.get("[role='rowgroup'] [role='row']")
.should('have.length', 50)
.then($rows => {
expect($rows).to.contain('Amy');
});
// checking the paginated data
cy.get(".pagination-container [role='navigation'] [role='button']")
.should('have.length', 9)
.then($pages => {
expect($pages).to.contain('1');
expect($pages).to.contain('729');
});
cy.get(".pagination-container [role='navigation'] [role='button']")
.eq(7)
.click();
cy.wait('@samples');
cy.get("[role='rowgroup'] [role='row']")
.should('have.length', 46)
.then($rows => {
expect($rows).to.contain('Victoria');
});
});
it('clears filters', () => {
interceptSamples();
// opens the modal by clicking on the box on the chart
cy.get("[data-test-viz-type='box_plot'] canvas").then($canvas => {
const canvasWidth = $canvas.width() || 0;
const canvasHeight = $canvas.height() || 0;
const canvasCenterX = canvasWidth / 6;
const canvasCenterY = canvasHeight / 6;
cy.wrap($canvas)
.scrollIntoView()
.rightclick(canvasCenterX, canvasCenterY, { force: true });
openModalFromChartContext('Drill to detail by East Asia & Pacific');
// checking the filter
cy.get("[data-test='filter-val']").should(
'contain',
'East Asia & Pacific',
);
cy.get("[data-test='row-count-label']").should('contain', '1.98k rows');
cy.get(".pagination-container [role='navigation'] [role='button']")
.should('have.length', 9)
.then($pages => {
expect($pages).to.contain('1');
expect($pages).to.contain('40');
});
// close the filter and test that data was reloaded
cy.get("[data-test='filter-col']").find("[aria-label='close']").click();
cy.wait('@samples');
cy.get("[data-test='row-count-label']").should('contain', '11.8k rows');
cy.get(".pagination-container [role='navigation'] li.active").should(
'contain',
'1',
);
cy.get(".pagination-container [role='navigation'] [role='button']")
.should('have.length', 9)
.then($pages => {
expect($pages).to.contain('1');
expect($pages).to.contain('236');
});
});
});
describe('Time-series Bar Chart V2', () => {
it('opens the modal with the correct filters', () => {
interceptSamples();
cy.get("[data-test-viz-type='echarts_timeseries_bar'] canvas").then(
$canvas => {
cy.wrap($canvas)
.scrollIntoView()
.rightclick(70, 100, { force: true });
cy.get('.ant-dropdown')
.not('.ant-dropdown-hidden')
.find("[role='menu'] [role='menuitem']")
.should('have.length', 3)
.then($menuitems => {
expect($menuitems).to.contain('Drill to detail by 1965');
expect($menuitems).to.contain('Drill to detail by boy');
expect($menuitems).to.contain('Drill to detail by all');
})
.eq(2)
.click();
cy.wait('@samples');
cy.get("[data-test='filter-val']").then($filters => {
expect($filters).to.contain('1965');
expect($filters).to.contain('boy');
});
},
);
});
});
describe('Box plot', () => {
it('opens the modal with the correct filters', () => {
interceptSamples();
// opens the modal by clicking on the box on the chart
cy.get("[data-test-viz-type='box_plot'] canvas").then($canvas => {
const canvasWidth = $canvas.width() || 0;
const canvasHeight = $canvas.height() || 0;
const canvasCenterX = canvasWidth / 6;
const canvasCenterY = canvasHeight / 6;
cy.wrap($canvas)
.scrollIntoView()
.rightclick(canvasCenterX, canvasCenterY, { force: true });
openModalFromChartContext('Drill to detail by East Asia & Pacific');
// checking the filter
cy.get("[data-test='filter-val']").should(
'contain',
'East Asia & Pacific',
);
});
});
});
describe('Pie', () => {
it('opens the modal with the correct filters', () => {
interceptSamples();
// opens the modal by clicking on the slice of the Pie chart
cy.get("[data-test-viz-type='pie'] canvas").then($canvas => {
const canvasWidth = $canvas.width() || 0;
const canvasHeight = $canvas.height() || 0;
const canvasCenterX = canvasWidth / 2;
const canvasCenterY = canvasHeight / 2;
cy.wrap($canvas)
.scrollIntoView()
.rightclick(canvasCenterX, canvasCenterY, { force: true });
openModalFromChartContext('Drill to detail by boy');
// checking the filtered and paginated data
cy.get("[data-test='filter-val']").should('contain', 'boy');
});
});
});
describe('Big number total', () => {
it('opens the modal with no filters', () => {
interceptSamples();
// opens the modal by clicking on the number on the chart
cy.get(
"[data-test-viz-type='big_number_total'] .header-line",
).rightclick();
openModalFromChartContext('Drill to detail');
cy.get("[data-test='filter-val']").should('not.exist');
});
});
describe('Big number with trendline', () => {
it('opens the modal with the correct data', () => {
interceptSamples();
// opens the modal by clicking on the number
cy.get("[data-test-viz-type='big_number'] .header-line").rightclick();
openModalFromChartContext('Drill to detail');
cy.get("[data-test='filter-val']").should('not.exist');
// TODO: test clicking on a trendline
// Cypress is refusing to rightclick on the dot
});
});
});

View File

@ -195,6 +195,7 @@ export default function transformProps(
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 10,
showSymbol: false,
color: mainColor,
areaStyle: {

View File

@ -105,6 +105,7 @@ export default function TableControls({
margin-bottom: ${theme.gridUnit * 4}px;
line-height: 1.2;
`}
data-test="filter-col"
>
<span
css={css`
@ -113,7 +114,7 @@ export default function TableControls({
>
{colName}
</span>
<strong>{val}</strong>
<strong data-test="filter-val">{val}</strong>
</Tag>
))}
</div>

View File

@ -35,10 +35,14 @@ export default function RowCountLabel(props: RowCountLabelProps) {
limitReached || (rowcount === 0 && !loading) ? 'danger' : 'default';
const formattedRowCount = getNumberFormatter()(rowcount);
const label = (
<Label type={type} data-test="row-count-label">
{loading
? t('Loading...')
: tn('%s row', '%s rows', rowcount, formattedRowCount)}
<Label type={type}>
{loading ? (
t('Loading...')
) : (
<span data-test="row-count-label">
{tn('%s row', '%s rows', rowcount, formattedRowCount)}
</span>
)}
</Label>
);
return limitReached ? (

View File

@ -54,6 +54,9 @@ def load_examples_run(
if load_test_data:
print("Loading [Tabbed dashboard]")
examples.load_tabbed_dashboard(only_metadata)
print("Loading [ECharts Dashboard]")
examples.load_echarts_dashboard()
else:
print("Loading [Random long/lat data]")
examples.load_long_lat_data(only_metadata, force)

View File

@ -21,6 +21,7 @@ from .birth_names import load_birth_names
from .country_map import load_country_map_data
from .css_templates import load_css_templates
from .deck import load_deck_dash
from .echarts_dashboard import load_echarts_dashboard
from .energy import load_energy
from .flights import load_flights
from .long_lat import load_long_lat_data

View File

@ -0,0 +1,250 @@
# 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.
import json
import textwrap
from typing import List
from sqlalchemy import inspect
from superset import db, security_manager
from superset.connectors.sqla.models import SqlaTable
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.utils.core import DatasourceType
from ..utils.database import get_example_database
from .helpers import (
get_slice_json,
get_table_connector_registry,
merge_slice,
update_slice_ids,
)
DASH_SLUG = "echarts_dash"
def create_slices(tbl: SqlaTable) -> List[Slice]:
admin = security_manager.find_user("admin")
slice_props = dict(
datasource_id=tbl.id,
datasource_type=DatasourceType.TABLE,
owners=[admin],
created_by=admin,
)
defaults = {
"limit": "25",
"time_range": "100 years ago : now",
"granularity_sqla": "ds",
"groupby": ["gender"],
"row_limit": "50000",
"viz_type": "echarts_timeseries_bar",
}
slices = [
Slice(
**slice_props,
slice_name="Time-Series Bar Chart V2",
viz_type="echarts_timeseries_bar",
params=get_slice_json(
defaults,
adhoc_filters=[
{
"clause": "WHERE",
"expressionType": "SIMPLE",
"filterOptionName": "filter_i7pmq9ob0vg_lvnj4s14yt",
"comparator": "10000",
"operator": ">",
"subject": "num_boys",
}
],
viz_type="dist_bar",
metrics=["sum__num"],
groupby=["gender"],
),
),
]
for slc in slices:
merge_slice(slc)
return slices
def load_echarts_dashboard() -> None:
"""Loading a dashboard featuring EChart charts"""
database = get_example_database()
engine = database.get_sqla_engine()
schema = inspect(engine).default_schema_name
tbl_name = "birth_names"
table_exists = database.has_table_by_name(tbl_name, schema=schema)
if table_exists:
table = get_table_connector_registry()
obj = (
db.session.query(table)
.filter_by(table_name=tbl_name, schema=schema)
.first()
)
create_slices(obj)
print("Creating the dashboard")
db.session.expunge_all()
dash = db.session.query(Dashboard).filter_by(slug=DASH_SLUG).first()
if not dash:
dash = Dashboard()
js = textwrap.dedent(
"""\
{
"CHART-dxV7Il74hH": {
"children": [],
"id": "CHART-dxV7Il74hH",
"meta": {
"chartId": 597,
"height": 50,
"sliceName": "Box plot",
"width": 6
},
"type": "CHART"
},
"CHART-YyHWQacdcj": {
"children": [],
"id": "CHART-YyHWQacdcj",
"meta": {
"chartId": 15,
"height": 50,
"sliceName": "Participants",
"width": 6
},
"type": "CHART"
},
"CHART-oWKBOJ6Ydh": {
"children": [],
"id": "CHART-oWKBOJ6Ydh",
"meta":{
"chartId": 16,
"height": 50,
"sliceName": "Genders",
"width": 6
},
"type": "CHART"
},
"CHART-06Kg-rUggO": {
"children": [],
"id": "CHART-06Kg-rUggO",
"meta": {
"chartId": 617,
"height": 50,
"sliceName": "Number of Girls",
"width": 6
},
"type": "CHART"
},
"CHART--wEhS-MDSg": {
"children": [],
"id": "CHART--wEhS-MDS",
"meta": {
"chartId": 2,
"height": 50,
"sliceName": "Energy Force Layout",
"width": 6
},
"type": "CHART"
},
"CHART--LXvS-RDSu": {
"children": [],
"id": "CHART--LXvS-RDSu",
"meta": {
"chartId": 398,
"height": 50,
"sliceName": "Time-Series Bar Chart V2",
"width": 6
},
"type": "CHART"
},
"GRID_ID": {
"children": [
"ROW-SytNzNA4X",
"ROW-HkFFEzVRVm",
"ROW-BytNzNA4Y"
],
"id": "GRID_ID",
"type": "GRID"
},
"HEADER_ID": {
"id": "HEADER_ID",
"meta": {
"text": "ECharts Dashboard"
},
"type": "HEADER"
},
"ROOT_ID": {
"children": [
"GRID_ID"
],
"id": "ROOT_ID",
"type": "ROOT"
},
"ROW-HkFFEzVRVm": {
"children": [
"CHART-dxV7Il74hH",
"CHART-oWKBOJ6Ydh"
],
"id": "ROW-HkFFEzVRVm",
"meta": {
"background": "BACKGROUND_TRANSPARENT"
},
"type": "ROW"
},
"ROW-SytNzNA4X": {
"children": [
"CHART-06Kg-rUggO",
"CHART-YyHWQacdcj"
],
"id": "ROW-SytNzNA4X",
"meta": {
"background": "BACKGROUND_TRANSPARENT"
},
"type": "ROW"
},
"ROW-BytNzNA4Y": {
"children": [
"CHART--wEhS-MDSg",
"CHART--LXvS-RDSu"
],
"id": "ROW-BytNzNA4Y",
"meta": {
"background": "BACKGROUND_TRANSPARENT"
},
"type": "ROW"
},
"DASHBOARD_VERSION_KEY": "v2"
}
"""
)
pos = json.loads(js)
dash.slices = update_slice_ids(pos)
dash.dashboard_title = "ECharts Dashboard"
dash.position_json = json.dumps(pos, indent=4)
dash.slug = DASH_SLUG
db.session.commit()

View File

@ -70,6 +70,7 @@ FEATURE_FLAGS = {
"ENABLE_TEMPLATE_PROCESSING": True,
"ALERT_REPORTS": True,
"DASHBOARD_NATIVE_FILTERS": True,
"DRILL_TO_DETAIL": True,
}