Files
metabase/e2e/support/helpers/e2e-visual-tests-helpers.js
2026-08-29 09:53:45 +12:00

306 lines
8.0 KiB
JavaScript

import { popover } from "e2e/support/helpers/e2e-ui-elements-helpers";
import { color as getColor } from "metabase/ui/colors";
import {
CHART_STYLE,
GOAL_LINE_DASH,
TREND_LINE_DASH,
} from "metabase/viz-core";
import { isFixedPositionElementVisible } from "./e2e-element-visibility-helpers";
export function ensureChartIsActive() {
cy.findAllByTestId("debounced-frame-root").each(($el) => {
cy.wrap($el).should("not.have.css", "pointer-events", "none");
});
cy.wait(500); // Let DebouncedFrame transitions settle
ensureEchartsContainerHasSvg();
}
export function echartsContainer() {
return cy.findByTestId("chart-container");
}
export function echartsTriggerBlur() {
echartsContainer().realHover({ position: "right" });
cy.wait(700); // Waiting until tooltip disappears
}
export function ensureEchartsContainerHasSvg() {
return echartsContainer().should((root) => {
// Check if there's an SVG child within the element
expect(root.find("svg").length, "SVG exists").to.be.equal(1);
});
}
export function goalLine() {
return echartsContainer().find(
`path[stroke-dasharray='${GOAL_LINE_DASH.join(",")}']`,
);
}
export function trendLine() {
return echartsContainer().find(
`path[stroke-dasharray='${TREND_LINE_DASH.join(",")}']`,
);
}
export function getXYTransform(element) {
const transform = element.prop("transform");
const {
baseVal: [{ matrix }],
} = transform;
const { e: x, f: y } = matrix;
return { x, y };
}
export function timelineEventChip(label) {
return cy.get(`[data-testid="timeline-event-chip"][aria-label="${label}"]`);
}
export function timelineEventMarkerLine() {
const lineWidth = CHART_STYLE.timelineEvents.selectionLineWidth;
return echartsContainer()
.find(`path[stroke-width='${lineWidth}'][fill='none']`)
.filter((_, element) => {
// a vertical line is drawn as "M x y1 L x y2" — two points with the same x
const coordinates = (element.getAttribute("d") ?? "")
.split(/[ML\s,]+/)
.filter(Boolean);
const [x1, , x2] = coordinates;
return coordinates.length === 4 && x1 === x2;
});
}
export function chartGridLines() {
return echartsContainer().find(
"path[stroke='var(--mb-color-cartesian-grid-line)'][fill='none']",
);
}
export function splitPanelAxisLines() {
const borderStrong = getColor("border-strong");
return echartsContainer().find(`path[stroke="${borderStrong}"]`);
}
export function chartPathWithFillColor(color) {
return echartsContainer().find(`path[fill="${color}"]`);
}
export function sankeyEdge(color) {
return echartsContainer().find(`path[fill="${color}"][fill-opacity="0.2"]`);
}
export function chartPathsWithFillColors(colors) {
return colors.map((color) => chartPathWithFillColor(color));
}
const CIRCLE_PATH = "M1 0A1 1 0 1 1 1 -0.0001";
const DIAMOND_PATH = "M0 -1L1 0L0 1L-1 0Z";
export function cartesianChartCircles() {
return echartsContainer().find(`path[d="${CIRCLE_PATH}"]`);
}
export function cartesianChartCircle() {
return cartesianChartCircles().should("be.visible");
}
export function cartesianChartCircleWithColor(color) {
return echartsContainer()
.find(`path[d="${CIRCLE_PATH}"][stroke="${color}"]`)
.should("be.visible");
}
export function cartesianChartCircleWithColors(colors) {
return colors.map((color) => cartesianChartCircleWithColor(color));
}
export function cartesianChartCircleWithFillColor(color) {
return echartsContainer()
.find(`path[d="${CIRCLE_PATH}"]`)
.filter((_, element) => getComputedStyle(element).fill === color);
}
export function otherSeriesChartPaths() {
return chartPathWithFillColor("#949AAB");
}
export function scatterBubbleWithColor(color) {
return echartsContainer().find(`path[d="${CIRCLE_PATH}"][fill="${color}"]`);
}
export function getValueLabels() {
return echartsContainer().find("text[stroke-width='3']");
}
export function testPairedTooltipValues(val1, val2) {
cy.contains(val1).closest("td").siblings("td").findByText(val2);
}
export function testTooltipPairs(rowPairs = []) {
popover().within(() => {
rowPairs.forEach(([label, value]) => {
testPairedTooltipValues(label, value);
});
});
}
export function testStackedTooltipRows(rows = []) {
popover().within(() => {
rows.forEach(([label, value, percent]) => {
cy.findByText(label)
.parent()
.within(() => {
cy.findByTestId("row-value").should("have.text", value);
cy.findByTestId("row-percent").should("have.text", percent);
});
});
});
}
export function pieSlices() {
return echartsContainer().find("path[stroke-linejoin='bevel']");
}
export function pieSliceWithColor(color) {
return echartsContainer().find(
`path[stroke-linejoin='bevel'][fill='${color}']`,
);
}
export function echartsTooltip() {
// ECharts may keep two dom instances of the tooltip
return cy.findAllByTestId("echarts-tooltip").should(($elements) => {
// Use a custom function to check if the fixed-position tooltip is visible,
// as Cypress's ":visible" or "be.visible" fails to identify a fixed-position tooltip as visible.
const visibleTooltips = $elements
.toArray()
.filter(isFixedPositionElementVisible);
// Assert we have exactly one visible tooltip
expect(visibleTooltips).to.have.length(
1,
"there must be only one visible echarts tooltip",
);
const visibleTooltip = visibleTooltips[0];
const tooltipContainerStyle = window.getComputedStyle(
visibleTooltip.closest(".echarts-tooltip-container"),
);
// (metabase#51904): tooltip container must render above the fold in the Embedding SDK.
// ensures that we are using fixed-positioned tooltips.
expect(tooltipContainerStyle.position).to.equal("fixed");
// (metabase#52732): tooltip container must have the correct z-index (201)
// this assertion prevents the tooltip from being rendered below charts or modals.
expect(Number(tooltipContainerStyle.zIndex)).to.equal(201);
// Return the visible tooltip
return visibleTooltip;
});
}
export function tooltipHeader() {
return cy.findByTestId("echarts-tooltip-header");
}
function tooltipFooter() {
return cy.findByTestId("echarts-tooltip-footer");
}
export function assertTooltipRow(
name,
{ color, value, secondaryValue, index } = {},
) {
// eslint-disable-next-line metabase/no-unsafe-element-filtering
cy.findAllByText(name)
.eq(index ?? 0)
.parent("tr")
.within(() => {
if (color) {
cy.get("td")
.eq(0)
.find("span")
.should("have.class", `marker-${color.replace("#", "")}`);
}
if (value) {
cy.findByText(value);
}
if (secondaryValue) {
cy.findByText(secondaryValue);
}
});
}
function assertTooltipFooter({ name, value, secondaryValue }) {
tooltipFooter().within(() => {
if (name) {
cy.findByText(name);
}
if (value) {
cy.findByText(value);
}
if (secondaryValue) {
cy.findByText(secondaryValue);
}
});
}
export function assertEChartsTooltip({ header, rows, footer, blurAfter }) {
echartsTooltip().within(() => {
if (header != null) {
tooltipHeader().should("have.text", header);
}
if (rows != null) {
rows.forEach((row) => {
const { name, ...rest } = row;
assertTooltipRow(name, rest);
});
}
if (footer != null) {
assertTooltipFooter(footer);
}
});
if (blurAfter) {
echartsTriggerBlur();
}
}
export function assertEChartsTooltipNotContain(rows) {
echartsTooltip().within(() => {
rows.forEach((row) => {
cy.findByText(row).should("not.exist");
});
});
}
export const BoxPlot = {
getBoxes() {
return echartsContainer().find('path[fill-opacity="0.15"][stroke]');
},
getPoints() {
return echartsContainer().find(`path[d="${CIRCLE_PATH}"]`);
},
getMeanMarkers() {
return echartsContainer().find(`path[d="${DIAMOND_PATH}"]`);
},
};
export function applyBrush(left, right) {
echartsContainer()
.trigger("mousedown", left, 100)
.trigger("mousemove", right, 100)
.trigger("mouseup", right, 100);
}