Consolidate metrics e2e tests (#83411)

* Remove duplicate and vacuous metrics e2e tests

Drop tests whose every assertion is repeated by a retained e2e test with
the same shape (metric rename, cancel-editing, add-to-dashboard from the
metric page, palette search via a recents-titled test, categorical
breakout), and two explorer tests whose assertions target strings the
app no longer renders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fold subset metrics e2e tests into their supersets

Move the few assertions unique to small tests into the tests that
already run the same setup and steps: explorer empty state, unified and
auto-split visualization checks, per-metric accordion, browse sidebar
link, browse listing, verified-filter absence, pinned metrics listing,
metric page tabs, the summarize step header, and first-stage action
buttons for metric-based questions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Repair metrics e2e assertions that cannot fail

Give negatives something to catch (create the model-based metric the
picker must exclude, look for the metric where it could appear, set the
verified-only setting to a non-default value), assert outcomes instead
of pre-existing state (duplicate navigates to the copy, tooltip content,
filtered chart values, literal formula terms), restore the 44171 trigger
by running the editor preview before saving, pin the temporal breakout
years to the sample data, and drop assertions that are always true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Strengthen weak metrics e2e assertions

Check that saved metric definitions keep their filters, joins and
expressions, anchor negatives on mounted UI, assert counts after adds
and removals, verify the picked per-metric dimension and brush filter
state, prove fixture dimensions exist before asserting they are not
shared, and make one-shot legend reads retry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Merge metrics e2e tests that share setup

Run behaviours that start from the same fixtures, user and page as one
test each: metric editor leave guard with save/cancel, dimension
search/reorder with curation, new-metric empty-state run with filtered
timeseries creation, compatible-metric aggregation listing/description/
search, browse empty state with cancel-discard and create permissions,
temporal unit with breakout replacement, browse navigation, dot-menu
flows per role, global and search-page metric search, collection
bookmark and trash flows, expression pill rename/revert, and curated
dimension listing with replacement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop unused setup from metrics e2e specs

Remove intercepts and aliases nothing waits on, intercepts re-registered
by nested hooks, a repeated sign-in, a no-op wait, a dead guard and a
redundant negative, and call existing helpers instead of inline copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Align metrics e2e titles and logs with what tests check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tighten waits and anchors in merged metrics e2e tests

Wait on search-page requests only, move the pointer off the hovercard
before searching the aggregation picker, and require the history tab to
list the revisions the test itself creates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Restore metrics e2e checks lost in consolidation

Assert that two metrics on a line or bar chart render as one
visualization, keep the metric summarize step header checked at a small
viewport, and check that seeding leaves no default dimension before any
edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Merge more metrics e2e tests that share setup

Fold the metric bookmark test's events into the collection and browse
bookmark flows and its metric-page bookmark into the scalar metric test,
run both metric drill-thrus from one question, add the alert setup and
trash steps to the metric definition test, add dashboard metrics via the
context menu and the sidebar in one test, and chain the numeric breakout
after the temporal one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix metrics e2e failures from consolidation

Restore the /api/card/*/query intercept the visualizer selectDataset
helper waits on, scope the dashboard text queries to the grid since
chained queries only search the first dashcard, and assert the numeric
metric math on the chart, as the pill suffixes repeated metric instances.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Address metrics e2e consolidation review

Restore the normal-user rename path and the empty pinned-section check
after unpinning the last metric. Merge the verified-filter toggle and
persisted-setting tests (now persisted via the UI toggle and verified
by reload), fold the static column-label check into the curated
dimensions test, and combine the EE dependencies and usage analytics
tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nemanja Glumac
2026-10-02 09:34:58 +02:00
committed by GitHub
parent cdbf081c75
commit 93d8358ed0
10 changed files with 658 additions and 970 deletions

View File

@@ -90,37 +90,48 @@ describe("scenarios > browse > metrics", () => {
beforeEach(() => {
H.restore();
cy.signInAsNormalUser();
cy.intercept("POST", "/api/dataset").as("dataset");
});
describe("no metrics", () => {
it("should not hide the browse metrics link in the sidebar", () => {
cy.visit("/");
H.navigationSidebar().findByText("Metrics").should("be.visible");
});
it("should show the empty metrics page, discard a new metric on cancel (metabase#48024), and hide create actions from users who cannot create queries", () => {
const emptyStateText =
"Create Metrics to define the official way to calculate important numbers for your team";
it("should show the empty metrics page", () => {
cy.visit("/browse/metrics");
cy.visit("/");
H.navigationSidebar().findByText("Metrics").should("be.visible").click();
cy.location("pathname").should("eq", "/browse/metrics");
cy.findByTestId("browse-metrics-header")
.findByLabelText("Create a new metric")
.should("be.visible");
H.main().within(() => {
cy.findByText(
"Create Metrics to define the official way to calculate important numbers for your team",
).should("be.visible");
cy.findByText(emptyStateText).should("be.visible");
cy.findByText("Create metric").should("be.visible").click();
});
cy.location("pathname").should("eq", "/metric/new");
});
it("should not show the create metric button if the user does not have data access", () => {
cy.log("cancelling a new metric asks to discard it");
H.MetricPage.queryEditor().should("be.visible");
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
H.MetricPage.cancelButton().click();
H.modal().within(() => {
cy.findByText("Discard your changes?").should("be.visible");
cy.button("Discard changes").click();
});
cy.location("pathname").should("eq", "/browse/metrics");
H.main().findByText(emptyStateText).should("be.visible");
cy.log(
"create actions are hidden from a sandboxed user who cannot create queries",
);
cy.signInAsSandboxedUser();
cy.visit("/browse/metrics");
H.main().within(() => {
cy.findByText(
"Create Metrics to define the official way to calculate important numbers for your team",
).should("be.visible");
cy.findByText(emptyStateText).should("be.visible");
cy.findByText("Create metric").should("not.exist");
});
cy.log("New metric header button should not show either");
cy.findByTestId("browse-metrics-header")
.findByLabelText("Create a new metric")
.should("not.exist");
@@ -157,35 +168,7 @@ describe("scenarios > browse > metrics", () => {
});
describe("multiple metrics", () => {
it("can browse metrics", () => {
createMetrics(ALL_METRICS);
cy.visit("/browse/metrics");
H.navigationSidebar().findByText("Metrics").should("be.visible");
ALL_METRICS.forEach((metric) => {
findMetric(metric.name).should("be.visible");
});
});
it("should navigate to the metric when clicking a metric title", () => {
createMetrics([ORDERS_SCALAR_METRIC]);
cy.visit("/browse/metrics");
findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible").click();
cy.location("pathname").should("match", /^\/metric\//);
H.MetricPage.aboutPage().should("be.visible");
});
it("should navigate to that collection when clicking a collection title", () => {
createMetrics([ORDERS_SCALAR_METRIC]);
cy.visit("/browse/metrics");
findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible");
metricsTable().findByText("Our analytics").should("be.visible").click();
cy.location("pathname").should("eq", "/collection/root");
});
it("should open the collections in a new tab when alt-clicking a metric", () => {
it("should open a metric in a new tab on meta-click, and navigate to its collection and to the metric on click", () => {
cy.on("window:before:load", (win) => {
// prevent Cypress opening in a new window/tab and spy on this method
cy.stub(win, "open").as("open");
@@ -194,6 +177,7 @@ describe("scenarios > browse > metrics", () => {
createMetrics([ORDERS_SCALAR_METRIC]);
cy.visit("/browse/metrics");
cy.log("meta-click opens the metric in a new tab");
findMetric(ORDERS_SCALAR_METRIC.name)
.should("be.visible")
.click(H.holdMetaKey);
@@ -207,6 +191,16 @@ describe("scenarios > browse > metrics", () => {
// the page did not navigate on this page
cy.location("pathname").should("eq", "/browse/metrics");
cy.log("clicking the collection navigates to it");
metricsTable().findByText("Our analytics").should("be.visible").click();
cy.location("pathname").should("eq", "/collection/root");
cy.log("clicking the metric navigates to it");
cy.visit("/browse/metrics");
findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible").click();
cy.location("pathname").should("match", /^\/metric\//);
H.MetricPage.aboutPage().should("be.visible");
});
it("should render truncated name and markdown in the table", () => {
@@ -216,6 +210,7 @@ describe("scenarios > browse > metrics", () => {
"This is a _very_ **long description** that should be truncated by the metrics table because it is really very long.";
createMetrics([
...ALL_METRICS,
{
...ORDERS_SCALAR_METRIC,
name,
@@ -224,6 +219,11 @@ describe("scenarios > browse > metrics", () => {
]);
cy.visit("/browse/metrics");
H.navigationSidebar().findByText("Metrics").should("be.visible");
ALL_METRICS.forEach((metric) => {
findMetric(metric.name).should("be.visible");
});
metricsTable()
.findByText(name)
@@ -239,7 +239,10 @@ describe("scenarios > browse > metrics", () => {
.findByText(/This is a/)
.realHover();
cy.findAllByText(/should be truncated/).should("have.length", 2);
H.tooltip().within(() => {
cy.findByText(/should be truncated/).should("be.visible");
cy.get("strong").should("have.text", "long description");
});
});
it("should be possible to sort the metrics", () => {
@@ -275,28 +278,15 @@ describe("scenarios > browse > metrics", () => {
});
describe("dot menu", () => {
it("should be possible to bookmark a metrics from the dot menu", () => {
createMetrics([ORDERS_SCALAR_METRIC]);
beforeEach(() => {
H.resetSnowplow();
cy.signInAsAdmin();
H.enableTracking();
cy.signInAsNormalUser();
});
cy.visit("/browse/metrics");
shouldNotHaveBookmark(ORDERS_SCALAR_METRIC.name);
metricsTable().findByLabelText("Metric options").click();
H.popover().findByText("Bookmark").should("be.visible").click();
shouldHaveBookmark(ORDERS_SCALAR_METRIC.name);
metricsTable().findByLabelText("Metric options").click();
H.popover()
.findByText("Remove from bookmarks")
.should("be.visible")
.click();
shouldNotHaveBookmark(ORDERS_SCALAR_METRIC.name);
metricsTable().findByLabelText("Metric options").click();
H.popover().findByText("Bookmark").should("be.visible");
afterEach(() => {
H.expectNoBadSnowplowEvents();
});
it("should be possible to navigate to the collection from the dot menu", () => {
@@ -313,12 +303,33 @@ describe("scenarios > browse > metrics", () => {
);
});
it("should be possible to trash a metric from the dot menu when the user has write access", () => {
it("should be possible to bookmark, trash, and restore a metric from the dot menu when the user has write access", () => {
createMetrics([ORDERS_SCALAR_METRIC]);
cy.visit("/browse/metrics");
cy.log("bookmark and unbookmark");
metricsTable().findByLabelText("Metric options").click();
H.popover().findByText("Bookmark").should("be.visible").click();
shouldHaveBookmark(ORDERS_SCALAR_METRIC.name);
H.expectUnstructuredSnowplowEvent({
event: "bookmark_added",
event_detail: "metric",
triggered_from: "browse_metrics",
});
metricsTable().findByLabelText("Metric options").click();
H.popover()
.findByText("Remove from bookmarks")
.should("be.visible")
.click();
shouldNotHaveBookmark(ORDERS_SCALAR_METRIC.name);
cy.log("trash and restore");
metricsTable().findByLabelText("Metric options").click();
H.popover().findByText("Bookmark").should("be.visible");
H.popover().findByText("Move to trash").should("be.visible").click();
H.main()
@@ -340,38 +351,21 @@ describe("scenarios > browse > metrics", () => {
});
describe("when the user does not have write access", () => {
it("should not be possible to trash a metric from the dot menu when the user does not have write access", () => {
it("should be possible to bookmark a metric and navigate to its collection, but not trash it, from the dot menu", () => {
createMetrics([ORDERS_SCALAR_METRIC]);
cy.signIn("readonly");
cy.visit("/browse/metrics");
cy.log("trash is not offered");
metricsTable().findByLabelText("Metric options").click();
H.popover().findByText("Move to trash").should("not.exist");
});
H.popover().within(() => {
cy.findByText("Bookmark").should("be.visible");
cy.findByText("Move to trash").should("not.exist");
});
it("should be possible to navigate to the collection from the dot menu", () => {
createMetrics([ORDERS_SCALAR_METRIC]);
cy.signIn("readonly");
cy.visit("/browse/metrics");
metricsTable().findByLabelText("Metric options").click();
H.popover().findByText("Open collection").should("be.visible").click();
cy.location("pathname").should("eq", "/collection/root");
});
it("should be possible to bookmark a metrics from the dot menu", () => {
createMetrics([ORDERS_SCALAR_METRIC]);
cy.signIn("readonly");
cy.visit("/browse/metrics");
shouldNotHaveBookmark(ORDERS_SCALAR_METRIC.name);
metricsTable().findByLabelText("Metric options").click();
H.popover().findByText("Bookmark").should("be.visible").click();
cy.log("bookmark and unbookmark");
H.popover().findByText("Bookmark").click();
shouldHaveBookmark(ORDERS_SCALAR_METRIC.name);
@@ -383,8 +377,12 @@ describe("scenarios > browse > metrics", () => {
shouldNotHaveBookmark(ORDERS_SCALAR_METRIC.name);
cy.log("open collection");
metricsTable().findByLabelText("Metric options").click();
H.popover().findByText("Bookmark").should("be.visible");
H.popover().findByText("Open collection").should("be.visible").click();
cy.location("pathname").should("eq", "/collection/root");
});
});
});
@@ -395,16 +393,7 @@ describe("scenarios > browse > metrics", () => {
H.activateToken("pro-self-hosted");
});
it("should not show the verified metrics filter when there are no verified metrics", () => {
createMetrics(ALL_METRICS);
cy.visit("/browse/metrics");
cy.findByLabelText("Table of metrics").should("be.visible");
cy.findByLabelText(/show.*verified.*metrics/i).should("not.exist");
});
it("should show the verified metrics filter when there are verified metrics", () => {
it("should show the verified metrics filter when there are verified metrics, and persist its user setting", () => {
cy.intercept(
"PUT",
"/api/setting/browse-filter-only-verified-metrics",
@@ -415,26 +404,38 @@ describe("scenarios > browse > metrics", () => {
findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible");
findMetric(ORDERS_SCALAR_MODEL_METRIC.name).should("be.visible");
cy.findByLabelText(/show.*verified.*metrics/i).should("not.exist");
verifyMetric(ORDERS_SCALAR_METRIC);
verifiedMetricsSwitch().should("have.attr", "aria-selected", "true");
findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible");
findMetric(ORDERS_SCALAR_MODEL_METRIC.name).should("not.exist");
toggleVerifiedMetricsFilter();
cy.get<{ request: Request }>("@setSetting").should((xhr) => {
expect(xhr.request.body).to.deep.equal({ value: false });
});
cy.wait("@setSetting")
.its("request.body")
.should("deep.equal", { value: false });
findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible");
findMetric(ORDERS_SCALAR_MODEL_METRIC.name).should("be.visible");
toggleVerifiedMetricsFilter();
cy.get<{ request: Request }>("@setSetting").should((xhr) => {
expect(xhr.request.body).to.deep.equal({ value: true });
});
cy.wait("@setSetting");
cy.log("the setting survives a reload");
cy.reload();
verifiedMetricsSwitch().should("have.attr", "aria-selected", "false");
findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible");
findMetric(ORDERS_SCALAR_MODEL_METRIC.name).should("be.visible");
toggleVerifiedMetricsFilter();
cy.wait("@setSetting")
.its("request.body")
.should("deep.equal", { value: true });
findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible");
findMetric(ORDERS_SCALAR_MODEL_METRIC.name).should("not.exist");
cy.reload();
verifiedMetricsSwitch().should("have.attr", "aria-selected", "true");
findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible");
findMetric(ORDERS_SCALAR_MODEL_METRIC.name).should("not.exist");
@@ -443,40 +444,6 @@ describe("scenarios > browse > metrics", () => {
findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible");
findMetric(ORDERS_SCALAR_MODEL_METRIC.name).should("be.visible");
});
it("should respect the user setting on whether to only show verified metrics", () => {
cy.intercept("GET", "/api/session/properties", (req) => {
req.continue((res) => {
res.body["browse-filter-only-verified-metrics"] = true;
res.send();
});
});
createMetrics([ORDERS_SCALAR_METRIC, ORDERS_SCALAR_MODEL_METRIC]);
cy.visit("/browse/metrics");
verifyMetric(ORDERS_SCALAR_METRIC);
findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible");
cy.findByRole("switch", { name: /show.*verified.*metrics/i }).should(
"have.attr",
"aria-selected",
"true",
);
cy.intercept("GET", "/api/session/properties", (req) => {
req.continue((res) => {
res.body["browse-filter-only-verified-metrics"] = false;
res.send();
});
});
cy.visit("/browse/metrics");
cy.findByRole("switch", { name: /show.*verified.*metrics/i }).should(
"have.attr",
"aria-selected",
"false",
);
});
});
});
@@ -527,6 +494,10 @@ function unverifyMetric(metric: StructuredQuestionDetailsWithName) {
.click();
}
function verifiedMetricsSwitch() {
return cy.findByRole("switch", { name: /show.*verified.*metrics/i });
}
function toggleVerifiedMetricsFilter() {
cy.findByLabelText(/show.*verified.*metrics/i).click();
}

View File

@@ -1,6 +1,5 @@
const { H } = cy;
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
import {
TRUSTED_ORDERS_METRIC,
createLibraryWithItems,
@@ -87,7 +86,7 @@ describe("scenarios > metrics > metric page", () => {
H.expectNoBadSnowplowEvents();
});
it("should display scalar metric, edit name and description, explore link, and more menu actions", () => {
it("should display a scalar metric, edit its name and description, link to explore, navigate tabs, bookmark, and duplicate it", () => {
cy.intercept("PUT", "/api/card/*").as("updateCard");
cy.intercept("POST", "/api/card").as("createCard");
@@ -141,6 +140,27 @@ describe("scenarios > metrics > metric page", () => {
.findByDisplayValue("Renamed metric")
.should("be.visible");
cy.log("navigate between tabs");
H.MetricPage.aboutTab().should("be.visible");
H.MetricPage.overviewTab().should("be.visible");
H.MetricPage.definitionTab().should("be.visible");
H.MetricPage.historyTab().should("be.visible");
H.MetricPage.definitionTab().click();
H.MetricPage.queryEditor().should("be.visible");
H.getNotebookStep("data").findByText("Orders").should("be.visible");
H.MetricPage.historyTab().click();
cy.findAllByTestId("revision-history-event").should("have.length.gte", 3);
H.MetricPage.aboutTab().click();
H.MetricPage.aboutPage().should("be.visible");
cy.log("bookmark via more menu");
H.MetricPage.moreMenu().click();
H.popover().findByTextEnsureVisible("Bookmark").click();
H.navigationSidebar().findByText("Renamed metric").should("be.visible");
cy.log("duplicate via more menu");
H.MetricPage.moreMenu().click();
H.popover().findByText("Duplicate").click();
@@ -151,36 +171,12 @@ describe("scenarios > metrics > metric page", () => {
.type("Renamed metric copy");
cy.button("Duplicate").click();
});
cy.wait("@createCard");
H.MetricPage.aboutPage().should("be.visible");
});
it("should open alert channel setup modal from more menu when no channels configured", () => {
H.createQuestion(ORDERS_SCALAR_METRIC).then(({ body: metric }) => {
H.visitMetric(metric.id);
});
H.MetricPage.moreMenu().click();
H.popover().findByText("Create an alert").click();
H.modal().within(() => {
cy.findByText(
"To get notified when something happens, or to send this chart on a schedule, first set up email, Slack, or a webhook.",
).should("be.visible");
cy.findByText("Set up email")
.should("be.visible")
.closest("a")
.should("have.attr", "href", "/admin/settings/email");
cy.findByText("Set up Slack")
.should("be.visible")
.closest("a")
.should("have.attr", "href", "/admin/settings/slack");
cy.findByText("Add a webhook")
.should("be.visible")
.closest("a")
.should("have.attr", "href", "/admin/settings/webhooks");
cy.wait("@createCard").then(({ response }) => {
cy.location("pathname").should("eq", `/metric/${response?.body.id}`);
});
H.MetricPage.aboutPage()
.findByDisplayValue("Renamed metric copy")
.should("be.visible");
});
it(
@@ -222,41 +218,6 @@ describe("scenarios > metrics > metric page", () => {
},
);
it("should display timeseries metric and navigate between tabs", () => {
H.createQuestion(ORDERS_TIMESERIES_METRIC).then(({ body: metric }) => {
// Set default dimension so the metric can be previewed as a timeseries chart.
H.setMetricDefaultDimension(metric.id, "Created At");
H.visitMetric(metric.id);
});
H.MetricPage.aboutPage().should("be.visible");
cy.log("the curated default time dimension charts the metric");
H.MetricPage.aboutPage().within(() => {
cy.findByTestId("visualization-root")
.should("be.visible")
.and("have.attr", "data-viz-ui-name", "Line");
H.echartsContainer().should("be.visible");
cy.findByRole("button", { name: /^Select dimension: Created At/ }).should(
"be.visible",
);
});
H.MetricPage.aboutTab().should("be.visible");
H.MetricPage.overviewTab().should("be.visible");
H.MetricPage.definitionTab().should("be.visible");
H.MetricPage.historyTab().should("be.visible");
H.MetricPage.definitionTab().click();
H.MetricPage.queryEditor().should("be.visible");
H.getNotebookStep("data").findByText("Orders").should("be.visible");
H.MetricPage.historyTab().click();
cy.findAllByTestId("revision-history-event").should("have.length.gte", 1);
H.MetricPage.aboutTab().click();
H.MetricPage.aboutPage().should("be.visible");
});
it("should render curated dimension charts in order and load more", () => {
H.createQuestion(ORDERS_TIMESERIES_METRIC).then(({ body: metric }) => {
addOverviewDimensions(metric.id);
@@ -267,6 +228,7 @@ describe("scenarios > metrics > metric page", () => {
H.MetricPage.overviewPage().should("be.visible");
H.MetricPage.overviewPage().within(() => {
cy.findAllByText(/^By /).should("have.length", 4);
cy.findAllByText(/^By /).then(($cards) => {
expect($cards.map((_, card) => card.textContent).get()).to.deep.equal([
"By Subtotal",
@@ -306,14 +268,42 @@ describe("scenarios > metrics > metric page", () => {
});
});
it("should edit, save, and cancel metric definition changes", () => {
it("should discard unsaved changes on leaving (metabase#32037), cancel and save metric definition changes, surface a failed revert (UXW-310), offer alert channel setup when no channels are configured, and move the metric to trash", () => {
cy.intercept("PUT", "/api/card/*").as("updateCard");
H.createQuestion(ORDERS_SCALAR_METRIC).then(({ body: metric }) => {
cy.visit(`/metric/${metric.id}/query`);
H.createQuestion(ORDERS_SCALAR_METRIC, {
wrapId: true,
idAlias: "metricId",
});
cy.get<number>("@metricId").then((metricId) => {
cy.visit(`/metric/${metricId}/query`);
});
H.MetricPage.queryEditor().should("be.visible");
H.MetricPage.saveButton().should("not.exist");
cy.log("leaving with unsaved changes asks to discard them");
H.getNotebookStep("summarize").button("Count").click();
H.popover().within(() => {
cy.findByText("Sum of ...").click();
cy.findByText("Total").click();
});
H.MetricPage.saveButton().should("be.visible");
H.MetricPage.aboutTab().click();
H.modal().within(() => {
cy.findByText("Discard your changes?").should("be.visible");
cy.findByText("Discard changes").click();
});
H.MetricPage.aboutPage().should("be.visible");
cy.get<number>("@metricId").then((metricId) => {
cy.location("pathname").should("eq", `/metric/${metricId}`);
});
H.MetricPage.definitionTab().click();
H.MetricPage.queryEditor().should("be.visible");
H.getNotebookStep("summarize").findByText("Count").should("be.visible");
cy.log("cancel reverts changes");
H.getNotebookStep("summarize").button("Count").click();
@@ -353,39 +343,35 @@ describe("scenarios > metrics > metric page", () => {
cy.wait("@failedRevert");
H.undoToast().should("contain.text", "Cannot revert: missing metric");
});
it("should add metric to dashboard and move to trash via more menu", () => {
cy.intercept("PUT", "/api/card/*").as("updateCard");
H.createQuestion(ORDERS_SCALAR_METRIC, {
wrapId: true,
idAlias: "metricId",
});
cy.get<number>("@metricId").then((metricId) => {
H.visitMetric(metricId);
});
cy.log("add to dashboard");
cy.log("create an alert without channels offers channel setup");
H.MetricPage.aboutTab().click();
H.MetricPage.aboutPage().should("be.visible");
H.MetricPage.moreMenu().click();
H.popover().findByText("Add to a dashboard").click();
H.modal().within(() => {
cy.findByRole("heading", {
name: "Add this metric to a dashboard",
}).should("be.visible");
cy.findByText("Orders in a dashboard").click();
cy.button("Select").click();
});
cy.location("pathname").should(
"eq",
`/dashboard/${ORDERS_DASHBOARD_ID}-orders-in-a-dashboard`,
);
H.popover().findByText("Create an alert").click();
cy.log("move to trash");
cy.get<number>("@metricId").then((metricId) => {
H.visitMetric(metricId);
H.modal().within(() => {
cy.findByText(
"To get notified when something happens, or to send this chart on a schedule, first set up email, Slack, or a webhook.",
).should("be.visible");
cy.findByText("Set up email")
.should("be.visible")
.closest("a")
.should("have.attr", "href", "/admin/settings/email");
cy.findByText("Set up Slack")
.should("be.visible")
.closest("a")
.should("have.attr", "href", "/admin/settings/slack");
cy.findByText("Add a webhook")
.should("be.visible")
.closest("a")
.should("have.attr", "href", "/admin/settings/webhooks");
});
cy.realPress("Escape");
H.modal().should("not.exist");
cy.log("move to trash via more menu");
H.MetricPage.moreMenu().click();
H.popover().findByText("Move to trash").click();
H.modal().button("Move to trash").click();
@@ -393,14 +379,14 @@ describe("scenarios > metrics > metric page", () => {
H.main().findByText("This metric is in the trash.");
});
it("should restrict editing controls and definition tab for read-only users", () => {
cy.signInAsAdmin();
it("should hide editing controls and the overview and definition tabs from read-only users", () => {
H.createQuestion(ORDERS_SCALAR_METRIC).then(({ body: metric }) => {
cy.signIn("readonly");
H.visitMetric(metric.id);
cy.log("about page hides editing controls");
H.MetricPage.aboutPage().should("be.visible");
H.MetricPage.header().findByText("Orders count").should("be.visible");
cy.findByDisplayValue("Orders count").should("not.exist");
H.MetricPage.moreMenu().click();
H.popover().within(() => {
@@ -413,6 +399,7 @@ describe("scenarios > metrics > metric page", () => {
cy.log("overview and definition tabs are hidden for read-only users");
cy.realPress("Escape");
H.MetricPage.historyTab().should("be.visible");
H.MetricPage.overviewTab().should("not.exist");
H.MetricPage.definitionTab().should("not.exist");
});
@@ -442,14 +429,32 @@ describe("scenarios > metrics > metric page", () => {
cy.visit(`/data-studio/library/metrics/${metric.id}`);
H.MetricPage.aboutPage().should("be.visible");
H.MetricPage.moreMenu().click();
H.popover().findByText("Bookmark").should("be.visible");
H.popover().findByText("Open in Data Studio").should("not.exist");
});
});
it("should navigate to usage analytics dashboard from more menu", () => {
it("should show the Dependencies tab with dependency graph and navigate to usage analytics from more menu", () => {
H.createQuestion(ORDERS_SCALAR_METRIC).then(({ body: metric }) => {
H.waitForBackfillComplete();
H.visitMetric(metric.id);
H.MetricPage.aboutPageDescriptionSidebar().within(() => {
cy.findByText("Relationships").should("be.visible");
cy.findByText("No dependencies").should("be.visible");
cy.findByText("No charts use this metric").should("be.visible");
});
H.MetricPage.dependenciesTab().click();
H.DependencyGraph.graph().within(() => {
cy.findByText("Table");
cy.findByText("Orders").should("be.visible");
cy.findByText("Orders count").should("be.visible");
});
cy.log("usage analytics from more menu");
H.MetricPage.aboutTab().click();
H.MetricPage.aboutPage().should("be.visible");
H.MetricPage.moreMenu().click();
H.popover()
.findByText("Metric usage analytics")
@@ -467,25 +472,5 @@ describe("scenarios > metrics > metric page", () => {
H.main().findByText("Question overview").should("be.visible");
});
});
it("should show the Dependencies tab with dependency graph in EE", () => {
H.createQuestion(ORDERS_SCALAR_METRIC).then(({ body: metric }) => {
H.waitForBackfillComplete();
H.visitMetric(metric.id);
});
H.MetricPage.aboutPageDescriptionSidebar().within(() => {
cy.findByText("Relationships").should("be.visible");
cy.findByText("No dependencies").should("be.visible");
cy.findByText("No charts use this metric").should("be.visible");
});
H.MetricPage.dependenciesTab().click();
H.DependencyGraph.graph().within(() => {
cy.findByText("Table");
cy.findByText("Orders").should("be.visible");
cy.findByText("Orders count").should("be.visible");
});
});
});
});

View File

@@ -34,11 +34,18 @@ const ORDERS_TIMESERIES_METRIC = {
describe("scenarios > metrics > collection", () => {
beforeEach(() => {
H.resetSnowplow();
H.restore();
cy.signInAsAdmin();
H.enableTracking();
cy.signInAsNormalUser();
});
it("should show metrics in collections", () => {
afterEach(() => {
H.expectNoBadSnowplowEvents();
});
it("should show pinned metrics in a collection, and unpin and pin them", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
H.createQuestion(ORDERS_TIMESERIES_METRIC);
cy.visit("/collection/root");
@@ -47,24 +54,28 @@ describe("scenarios > metrics > collection", () => {
cy.findByText(ORDERS_TIMESERIES_METRIC.name).should("be.visible");
cy.findAllByText("A metric").should("have.length", 2).and("be.visible");
});
});
it("should be possible to pin and unpin metrics", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
cy.visit("/collection/root");
H.getPinnedSection()
.findByText(ORDERS_SCALAR_METRIC.name)
.should("be.visible");
// Pinned items also stay in the contents list below the pinned section
H.getUnpinnedSection()
.findByText(ORDERS_SCALAR_METRIC.name)
.should("be.visible");
H.openPinnedItemMenu(ORDERS_SCALAR_METRIC.name);
H.popover().findByText("Unpin").click();
H.getPinnedSection().should("not.exist");
H.getPinnedSection().within(() => {
cy.findByText(ORDERS_TIMESERIES_METRIC.name).should("be.visible");
cy.findByText(ORDERS_SCALAR_METRIC.name).should("not.exist");
});
H.getUnpinnedSection()
.findByText(ORDERS_SCALAR_METRIC.name)
.should("be.visible");
cy.log("unpinning the last pinned metric removes the pinned section");
H.openPinnedItemMenu(ORDERS_TIMESERIES_METRIC.name);
H.popover().findByText("Unpin").click();
H.getPinnedSection().should("not.exist");
H.getUnpinnedSection()
.findByText(ORDERS_TIMESERIES_METRIC.name)
.should("be.visible");
H.openUnpinnedItemMenu(ORDERS_SCALAR_METRIC.name);
H.popover().findByText("Pin this").click();
H.getPinnedSection()
@@ -75,13 +86,14 @@ describe("scenarios > metrics > collection", () => {
.should("be.visible");
});
it("should be possible to add and remove a metric from bookmarks", () => {
it("should be possible to bookmark, trash, restore, and permanently delete a metric", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
H.createQuestion({
...ORDERS_TIMESERIES_METRIC,
collection_position: null,
});
cy.log("add and remove bookmarks");
cy.visit("/collection/root");
H.getPinnedSection().should("contain", ORDERS_SCALAR_METRIC.name);
@@ -91,6 +103,11 @@ describe("scenarios > metrics > collection", () => {
H.navigationSidebar()
.findByText(ORDERS_SCALAR_METRIC.name)
.should("be.visible");
H.expectUnstructuredSnowplowEvent({
event: "bookmark_added",
event_detail: "metric",
triggered_from: "collection_list",
});
H.openPinnedItemMenu(ORDERS_SCALAR_METRIC.name);
H.popover().findByText("Remove from bookmarks").click();
@@ -103,21 +120,21 @@ describe("scenarios > metrics > collection", () => {
H.navigationSidebar()
.findByText(ORDERS_TIMESERIES_METRIC.name)
.should("be.visible");
H.expectUnstructuredSnowplowEvent(
{
event: "bookmark_added",
event_detail: "metric",
triggered_from: "collection_list",
},
2,
);
H.openUnpinnedItemMenu(ORDERS_TIMESERIES_METRIC.name);
H.popover().findByText("Remove from bookmarks").click();
H.navigationSidebar()
.findByText(ORDERS_TIMESERIES_METRIC.name)
.should("not.exist");
});
it("should be possible to archive, unarchive, and delete a metric", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
H.createQuestion({
...ORDERS_TIMESERIES_METRIC,
collection_position: null,
});
cy.visit("/collection/root");
cy.log("trash, restore, and permanently delete");
H.openPinnedItemMenu(ORDERS_SCALAR_METRIC.name);
H.popover().findByText("Move to trash").click();
H.getPinnedSection().should("not.exist");

View File

@@ -85,6 +85,26 @@ function curateMetricDimension(metricId, displayName) {
});
}
function assertDashboardHasTwoScalarMetricsAndATimeseriesMetric() {
H.getDashboardCards().should("have.length", 4);
H.dashboardGrid()
.findAllByText(ORDERS_SCALAR_METRIC.name)
.should("have.length", 2);
H.dashboardGrid()
.findAllByTestId("scalar-value")
.should("have.length", 2)
.each(($value) => {
cy.wrap($value).should("be.visible").and("have.text", "18,760");
});
H.getDashboardCards()
.filter(`:contains("${ORDERS_TIMESERIES_METRIC.name}")`)
.should("have.length", 1)
.within(() => {
cy.findByText(ORDERS_TIMESERIES_METRIC.name).should("be.visible");
H.echartsContainer().should("be.visible");
});
}
describe("scenarios > metrics > dashboard", () => {
beforeEach(() => {
H.restore();
@@ -151,7 +171,8 @@ describe("scenarios > metrics > dashboard", () => {
});
});
it("should be possible to add metric to a dashboard via context menu (metabase#44220)", () => {
it("should be possible to add metrics to a dashboard via the context menu (metabase#44220) and via the questions sidebar", () => {
H.createQuestion(ORDERS_TIMESERIES_METRIC);
H.createQuestion(ORDERS_SCALAR_METRIC).then(
({ body: { id: metricId } }) => {
H.visitMetric(metricId);
@@ -193,29 +214,23 @@ describe("scenarios > metrics > dashboard", () => {
});
},
);
});
it("should be possible to add metrics to a dashboard", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
H.createQuestion(ORDERS_TIMESERIES_METRIC);
H.visitDashboard(ORDERS_DASHBOARD_ID);
cy.log("Add scalar and timeseries metrics via the questions sidebar");
cy.intercept("GET", "/api/search?*").as("sidebarSearch");
H.editDashboard();
H.openQuestionsSidebar();
cy.findByTestId("add-card-sidebar").within(() => {
cy.findByText(ORDERS_SCALAR_METRIC.name).click();
cy.findByPlaceholderText("Search…").type(ORDERS_TIMESERIES_METRIC.name);
cy.wait("@search");
cy.wait("@sidebarSearch");
cy.findByText(ORDERS_SCALAR_METRIC.name).should("not.exist");
cy.findByText(ORDERS_TIMESERIES_METRIC.name).click();
});
H.getDashboardCard(1).within(() => {
cy.findByText(ORDERS_SCALAR_METRIC.name).should("be.visible");
cy.findByText("18,760").should("be.visible");
});
H.getDashboardCard(2).within(() => {
cy.findByText(ORDERS_TIMESERIES_METRIC.name).should("be.visible");
H.echartsContainer().should("be.visible");
});
assertDashboardHasTwoScalarMetricsAndATimeseriesMetric();
cy.log("Assert the sidebar-added metrics persist after saving");
H.saveDashboard();
assertDashboardHasTwoScalarMetricsAndATimeseriesMetric();
});
it("should use curated metric dimensions for dashboard filters (UXW-4770)", () => {
@@ -303,7 +318,7 @@ describe("scenarios > metrics > dashboard", () => {
H.saveDashboard();
H.getDashboardCard().within(() => {
H.cartesianChartCircle()
.eq(5) // random dot
.eq(5) // sixth point, whose Count of 92 is passed as the User ID
.click({ force: true });
});
cy.wait("@dataset");
@@ -338,9 +353,6 @@ describe("scenarios > metrics > dashboard default dimension", () => {
);
expect(totalDimension, "Total metric dimension").not.to.be.undefined;
if (!totalDimension) {
return;
}
return cy.request(
"POST",

View File

@@ -62,7 +62,7 @@ describe("scenarios > metrics > dimensions", () => {
H.expectNoBadSnowplowEvents();
});
it("curates the dimension list: seeded columns, add, rename, set default, and remove", () => {
it("curates the dimension list: seeded columns, search, drag-and-drop reorder, add, rename, describe, set and remove the default, and bulk remove", () => {
H.visitMetric(metricId);
H.MetricPage.dimensionsTab().click();
H.MetricPage.dimensionsPage().should("be.visible");
@@ -83,6 +83,54 @@ describe("scenarios > metrics > dimensions", () => {
.and("contain", "Quantity")
.and("not.contain", "Vendor");
cy.log("seeding leaves the metric without a default dimension");
dimensionList().findByText("Default").should("not.exist");
cy.log("search filters the added dimensions");
dimensionList().findByPlaceholderText("Search…").type("Disc");
allDimensionRows().should("have.length", 1);
dimensionRow("Discount").should("be.visible");
dimensionList().findByPlaceholderText("Search…").clear();
allDimensionRows().should("have.length", SEEDED_DIMENSIONS_COUNT);
cy.log("drag Quantity to the top of the list");
allDimensionRows().first().should("not.contain", "Quantity");
// Scoped through the list: during a drag the DragOverlay portal renders a
// clone of the row with the same test ids, and re-running an unscoped
// query would match both.
dimensionList()
.findByTestId("dimension-row-Quantity")
.findByTestId("dimension-drag-handle")
.as("dragHandle");
allDimensionRows()
.first()
.then(($firstRow) => {
dimensionRow("Quantity").then(($sourceRow) => {
// Overshoot past the first row's top so the dragged row's center
// clearly crosses above it.
const vertical =
$firstRow[0].getBoundingClientRect().top -
$sourceRow[0].getBoundingClientRect().top -
30;
H.moveDnDKitElementByAlias("@dragHandle", { vertical });
});
});
cy.wait("@reorderDimensions");
H.expectUnstructuredSnowplowEvent({
event: "metric_dimensions_reordered",
target_id: metricId,
result: "success",
});
allDimensionRows().first().should("contain", "Quantity");
cy.log("the new order is persisted");
cy.request<ListMetricDimensionsResponse>(
"GET",
`/api/metric/${metricId}/dimension`,
)
.its("body.added.0.display_name")
.should("equal", "Quantity");
cy.log("add a dimension from a connected table");
dimensionList()
.findByRole("button", { name: "Available dimensions" })
@@ -132,9 +180,6 @@ describe("scenarios > metrics > dimensions", () => {
2,
);
cy.log("seeding leaves the metric without a default dimension");
dimensionList().findByText("Default").should("not.exist");
cy.log("make it the default dimension");
settingsPanel().findByRole("button", { name: "Set as default" }).click();
cy.wait("@setDefaultDimension");
@@ -147,7 +192,6 @@ describe("scenarios > metrics > dimensions", () => {
.scrollIntoView()
.findByText("Default")
.should("be.visible");
dimensionRow("Created At").findByText("Default").should("not.exist");
settingsPanel().findByText("Default dimension").should("be.visible");
dimensionList().findAllByText("Default").should("have.length", 1);
@@ -204,57 +248,6 @@ describe("scenarios > metrics > dimensions", () => {
.and("not.contain", "Tax");
});
it("filters with search and persists a drag-and-drop reorder", () => {
cy.visit(`/metric/${metricId}/dimensions`);
cy.wait("@listDimensions");
allDimensionRows().should("have.length", SEEDED_DIMENSIONS_COUNT);
cy.log("search filters the added dimensions");
dimensionList().findByPlaceholderText("Search…").type("Disc");
allDimensionRows().should("have.length", 1);
dimensionRow("Discount").should("be.visible");
dimensionList().findByPlaceholderText("Search…").clear();
allDimensionRows().should("have.length", SEEDED_DIMENSIONS_COUNT);
cy.log("drag Quantity to the top of the list");
allDimensionRows().first().should("not.contain", "Quantity");
// Scoped through the list: during a drag the DragOverlay portal renders a
// clone of the row with the same test ids, and re-running an unscoped
// query would match both.
dimensionList()
.findByTestId("dimension-row-Quantity")
.findByTestId("dimension-drag-handle")
.as("dragHandle");
allDimensionRows()
.first()
.then(($firstRow) => {
dimensionRow("Quantity").then(($sourceRow) => {
// Overshoot past the first row's top so the dragged row's center
// clearly crosses above it.
const vertical =
$firstRow[0].getBoundingClientRect().top -
$sourceRow[0].getBoundingClientRect().top -
30;
H.moveDnDKitElementByAlias("@dragHandle", { vertical });
});
});
cy.wait("@reorderDimensions");
H.expectUnstructuredSnowplowEvent({
event: "metric_dimensions_reordered",
target_id: metricId,
result: "success",
});
allDimensionRows().first().should("contain", "Quantity");
cy.log("the new order is persisted");
cy.request<ListMetricDimensionsResponse>(
"GET",
`/api/metric/${metricId}/dimension`,
)
.its("body.added.0.display_name")
.should("equal", "Quantity");
});
it("uses a time dimension's configured bucket on the About page", () => {
cy.intercept("POST", "/api/metric/dataset", (request) => {
if (request.body.definition?.projections?.length > 0) {

View File

@@ -157,33 +157,26 @@ function verifyMetricDefinitionScalar({ aggregation, value }) {
describe("scenarios > metrics > editing", () => {
beforeEach(() => {
H.restore();
H.resetSnowplow();
cy.signInAsNormalUser();
});
describe("organization", () => {
it("should be able to rename a metric", () => {
cy.intercept("PUT", "/api/card/*").as("updateCard");
H.createQuestion(ORDERS_SCALAR_METRIC).then(({ body: card }) => {
H.visitMetric(card.id);
});
H.MetricPage.aboutPage()
.findByDisplayValue(ORDERS_SCALAR_METRIC.name)
.clear()
.type("New metric name{enter}");
cy.wait("@updateCard");
H.MetricPage.aboutPage()
.findByDisplayValue("New metric name")
.should("be.visible");
});
it("should be able to change the query definition of a metric", () => {
cy.intercept("PUT", "/api/card/*").as("updateCard");
it("should be able to change the query definition of a metric and rename it", () => {
H.createQuestion(ORDERS_SCALAR_METRIC).then(({ body: card }) => {
H.setMetricDefaultDimension(card.id, "Created At");
cy.visit(`/metric/${card.id}/query`);
});
H.MetricPage.queryEditor().should("be.visible");
H.getNotebookStep("summarize").findByText("Formula").should("be.visible");
cy.log("summarize step copy stays reachable at a small viewport");
cy.viewport(800, 600);
H.getNotebookStep("summarize")
.findByText("Formula")
.should("be.visible")
.scrollIntoView();
cy.viewport(1280, 800);
H.getNotebookStep("summarize").button("Count").click();
H.popover().within(() => {
cy.findByText("Sum of ...").click();
@@ -193,54 +186,31 @@ describe("scenarios > metrics > editing", () => {
H.MetricPage.saveButton().should("not.exist");
H.MetricPage.aboutTab().click();
verifyMetricAboutTimeseries({ yAxis: "Sum of Total" });
});
it("should not crash when cancelling creation or editing of a metric (metabase#48024)", () => {
cy.log("cancel new metric creation");
startNewMetricWithTable("Sample Database", "Orders");
H.MetricPage.cancelButton().click();
cy.log("cancel editing an existing metric");
H.createQuestion(ORDERS_SCALAR_METRIC).then(({ body: card }) =>
cy.visit(`/metric/${card.id}/query`),
);
H.MetricPage.queryEditor().should("be.visible");
H.getNotebookStep("summarize").button("Count").click();
H.popover().within(() => {
cy.findByText("Sum of ...").click();
cy.findByText("Total").click();
});
H.MetricPage.cancelButton().click();
H.getNotebookStep("summarize").findByText("Count").should("be.visible");
cy.log("rename the metric");
cy.intercept("PUT", "/api/card/*").as("updateCard");
H.MetricPage.aboutPage()
.findByDisplayValue(ORDERS_SCALAR_METRIC.name)
.clear()
.type("New metric name{enter}");
cy.wait("@updateCard")
.its("response.body.name")
.should("eq", "New metric name");
H.MetricPage.aboutPage()
.findByDisplayValue("New metric name")
.should("be.visible");
});
});
describe("data source", () => {
it("should create a metric based on a table", () => {
startNewMetricWithTable("Sample Database", "Orders");
addStringCategoryFilter({
tableName: "Product",
columnName: "Category",
values: ["Gadget"],
});
saveNewMetric();
verifyMetricAboutTimeseries({ yAxis: "Count" });
});
it("should not allow to create a multi-stage metric", () => {
startNewMetricWithSavedItem("Our analytics", "Orders Model");
H.getNotebookStep("data").findByText("Orders Model").should("be.visible");
H.getNotebookStep("summarize").findByText("Count").should("be.visible");
H.MetricPage.queryEditor()
.findByRole("button", { name: "Summarize" })
.should("not.exist");
});
it("should allow to run the query from the metric empty state", () => {
startNewMetricWithTable("Sample Database", "Orders");
cy.intercept("POST", "/api/dataset").as("dataset");
H.runButtonInOverlay().click();
cy.wait("@dataset");
verifyScalarValue("18,760");
});
});
describe("joins", () => {
@@ -260,6 +230,13 @@ describe("scenarios > metrics > editing", () => {
});
saveNewMetric();
verifyMetricAboutTimeseries({ yAxis: "Count" });
H.MetricPage.definitionTab().click();
H.getNotebookStep("join")
.findByLabelText("Right table")
.should("have.text", "Orders");
H.getNotebookStep("filter")
.findByText("User → State is CA")
.should("be.visible");
});
it("should not be possible to join a metric", () => {
@@ -274,24 +251,11 @@ describe("scenarios > metrics > editing", () => {
});
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Our analytics").click();
cy.findByText("Orders").should("be.visible");
cy.findByText(ORDERS_SCALAR_METRIC.name).should("not.exist");
});
});
it("should be possible to join data on the first stage of a metric-based query", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
H.startNewQuestion();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(ORDERS_SCALAR_METRIC.name).click();
});
H.getNotebookStep("data").within(() => {
getActionButton("Custom column").should("be.visible");
getActionButton("Join data").should("be.visible");
});
});
});
describe("custom columns", () => {
@@ -336,9 +300,22 @@ describe("scenarios > metrics > editing", () => {
});
});
describe("breakouts", () => {
it("should create a timeseries metric", () => {
describe("filters", () => {
it("should run the query from the empty state, then create a filtered timeseries metric based on a table", () => {
startNewMetricWithTable("Sample Database", "Orders");
cy.log("run the query from the metric empty state");
cy.intercept("POST", "/api/dataset").as("dataset");
H.runButtonInOverlay().click();
cy.wait("@dataset");
verifyScalarValue("18,760");
cy.log("filter and aggregate, then save");
addStringCategoryFilter({
tableName: "Product",
columnName: "Category",
values: ["Gadget"],
});
H.getNotebookStep("summarize").findByText("Count").click();
H.popover().within(() => {
cy.findByText("Sum of ...").click();
@@ -346,6 +323,10 @@ describe("scenarios > metrics > editing", () => {
});
saveNewMetric();
verifyMetricAboutTimeseries({ yAxis: "Sum of Total" });
H.MetricPage.definitionTab().click();
H.getNotebookStep("filter")
.findByText("Product → Category is Gadget")
.should("be.visible");
});
});
@@ -372,33 +353,19 @@ describe("scenarios > metrics > editing", () => {
H.popover().button("Update").should("not.be.disabled").click();
saveNewMetric();
verifyMetricAboutTimeseries({ yAxis: "Orders metric" });
});
it("should have metric-specific summarize step copy", () => {
H.createQuestion(ORDERS_SCALAR_METRIC).then(({ body: card }) =>
cy.visit(`/metric/${card.id}/query`),
);
H.MetricPage.queryEditor().should("be.visible");
H.getNotebookStep("summarize").within(() => {
cy.findByText("Formula").should("be.visible");
cy.findByText("Default time dimension").should("not.exist");
});
cy.viewport(800, 600);
H.getNotebookStep("summarize").within(() => {
// We need the scroll because of the viewport change, the next findByText is technically off the screen without it
cy.findByText("Formula").should("be.visible").scrollIntoView({});
cy.findByText("Default time dimension").should("not.exist");
verifyMetricDefinitionScalar({
aggregation: "Orders metric",
value: "9,380",
});
});
});
describe("compatible metrics", () => {
it("should allow adding an aggregation based on a compatible metric for the same table in questions (metabase#42470)", () => {
it("should list, describe, search, and add an aggregation based on a compatible metric for the same table in questions (metabase#42470)", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
H.createQuestion(ORDERS_SCALAR_FILTER_METRIC);
H.createQuestion(PRODUCTS_SCALAR_METRIC);
H.createQuestion(ORDERS_SCALAR_MODEL_METRIC);
H.startNewQuestion();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
@@ -411,51 +378,36 @@ describe("scenarios > metrics > editing", () => {
cy.findByText(ORDERS_SCALAR_FILTER_METRIC.name).should("be.visible");
cy.findByText(PRODUCTS_SCALAR_METRIC.name).should("not.exist");
cy.findByText(ORDERS_SCALAR_MODEL_METRIC.name).should("not.exist");
cy.findByText(ORDERS_SCALAR_METRIC.name).click();
});
H.visualize();
verifyScalarValue("18,760");
});
it("should for searching for metrics", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
H.createQuestion(ORDERS_SCALAR_FILTER_METRIC);
H.createQuestion(PRODUCTS_SCALAR_METRIC);
H.startNewQuestion();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
startNewAggregation();
cy.log("hovering a metric shows its description");
H.popover().within(() => {
cy.findByPlaceholderText("Find...").type("with filter");
cy.findByText("Metrics").should("be.visible");
cy.findByText(ORDERS_SCALAR_METRIC.name).should("not.exist");
cy.findByText(PRODUCTS_SCALAR_METRIC.name).should("not.exist");
cy.findByText(ORDERS_SCALAR_MODEL_METRIC.name).should("not.exist");
cy.findByText(ORDERS_SCALAR_FILTER_METRIC.name).should("be.visible");
});
});
it("should show the description for metrics", () => {
H.createQuestion(ORDERS_SCALAR_FILTER_METRIC);
H.startNewQuestion();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
startNewAggregation();
H.popover().within(() => {
cy.findByText("Metrics").click();
cy.findByText(ORDERS_SCALAR_FILTER_METRIC.name).should("be.visible");
cy.findByText(ORDERS_SCALAR_FILTER_METRIC.name).realHover();
cy.findByLabelText("More info").should("exist").realHover();
});
H.hovercard().within(() => {
cy.contains("This is a description").should("be.visible");
cy.contains("with markdown").should("be.visible");
});
H.popover().findByPlaceholderText("Find...").realHover();
H.hovercard().should("not.exist");
cy.log("search for metrics");
H.popover().within(() => {
cy.findByPlaceholderText("Find...").type("with filter");
cy.findByText("Metrics").should("be.visible");
cy.findByText(ORDERS_SCALAR_FILTER_METRIC.name).should("be.visible");
cy.findByText(ORDERS_SCALAR_METRIC.name).should("not.exist");
cy.findByText(PRODUCTS_SCALAR_METRIC.name).should("not.exist");
cy.findByPlaceholderText("Find...").clear();
});
cy.log("pick a metric");
H.popover().within(() => {
cy.findByText(ORDERS_SCALAR_METRIC.name).should("be.visible").click();
});
H.visualize();
verifyScalarValue("18,760");
});
});
});

View File

@@ -265,7 +265,6 @@ const selectBreakout = (
*/
const interceptDatasetQuery = () => {
cy.intercept("POST", "/api/metric/dataset").as("dataset");
cy.intercept("POST", "/api/metric/breakout-values").as("breakoutValues");
};
/**
@@ -508,8 +507,6 @@ describe("scenarios > metrics > explorer", () => {
interceptDatasetQuery();
cy.intercept("GET", "/api/metric/*").as("getMetric");
cy.intercept("GET", "/api/measure/*").as("getMeasure");
cy.intercept("GET", "/api/search*").as("search");
H.resetSnowplow();
H.enableTracking();
});
@@ -519,33 +516,7 @@ describe("scenarios > metrics > explorer", () => {
});
describe("Entry points", () => {
it("should show empty state on first load", () => {
H.MetricsViewer.goToViewer();
cy.url().should("include", "/explore");
cy.findByRole("heading", { name: "Start exploring" }).should(
"be.visible",
);
addMetric("Count of products");
H.echartsContainer().should("be.visible");
cy.log("should persist state in url");
cy.reload();
verifyMetricCount(1);
});
it("should not show Edit in Data Studio for users without data studio access", () => {
cy.signInAsNormalUser();
H.MetricsViewer.goToViewer();
addMetric("Count of orders");
H.MetricsViewer.searchBarPills().contains("Count of orders").click();
H.popover().should("not.contain", "Edit in Data Studio");
});
it("should handle breakout with no results gracefully", () => {
it("should show a No results state for a metric whose query returns no rows", () => {
createMetrics([
{
name: "Empty Metric",
@@ -565,8 +536,12 @@ describe("scenarios > metrics > explorer", () => {
});
describe("Adding metrics and measures", () => {
it("should add multiple metrics", () => {
it("should add metrics, duplicates, and measures, persist them in the URL, and show no search results for unknown names", () => {
H.MetricsViewer.goToViewer();
cy.url().should("include", "/explore");
cy.findByRole("heading", { name: "Start exploring" }).should(
"be.visible",
);
addMetric("Count of products");
@@ -575,14 +550,23 @@ describe("scenarios > metrics > explorer", () => {
event_detail: "metric",
});
H.echartsContainer().should("be.visible");
cy.log("should persist state in url");
cy.reload();
verifyMetricCount(1);
addMetric("Count of orders");
verifyMetricCount(2);
cy.log("allows duplicates");
addMetric("Count of products");
verifyMetricCount(3);
cy.log("Should allow me to add measures");
addMetricInputSequence([{ nameOrPath: testMeasurePath }]);
verifyMetricCount(4);
H.MetricsViewer.searchBarPills().should("contain.text", "Test Measure");
H.expectUnstructuredSnowplowEvent({
event: "metrics_viewer_metric_added",
event_detail: "measure",
@@ -661,14 +645,14 @@ describe("scenarios > metrics > explorer", () => {
addMetric("Count of orders");
});
it("should add a temporal breakout dimension", () => {
it("should add, change, and remove a temporal series breakout, then add a numeric breakout with default binning", () => {
cy.log("temporal breakout with year binning");
selectBreakout("Count of orders", "Created At", 0, "Year");
H.MetricsViewer.breakoutLegend().within(() => {
cy.findByRole("heading", { name: "Created At" }).should("be.visible");
const currentYear = new Date().getFullYear();
for (let year = 2025; year <= currentYear; year++) {
cy.findByText(String(year)).should("be.visible");
}
["2025", "2026", "2027", "2028", "2029"].forEach((year) => {
cy.findByText(year).should("be.visible");
});
});
H.MetricsViewer.searchBarPills()
@@ -677,6 +661,7 @@ describe("scenarios > metrics > explorer", () => {
.children()
.should("have.length", 5);
cy.log("change the series breakout");
H.MetricsViewer.searchBarPills().contains("Count of orders").click();
H.popover().findByText("Change series breakout").click();
H.popover().findByText("Category").click();
@@ -685,28 +670,12 @@ describe("scenarios > metrics > explorer", () => {
.findByRole("heading", { name: /Category/ })
.should("be.visible");
cy.log("remove the series breakout");
H.MetricsViewer.searchBarPills().contains("Count of orders").click();
H.popover().findByText("Remove series breakout").click();
H.MetricsViewer.breakoutLegend().should("not.exist");
});
it("should add a categorical breakout dimension", () => {
selectBreakout("Count of orders", "Source");
H.MetricsViewer.breakoutLegend()
.findByRole("heading", { name: /Source/ })
.should("be.visible");
H.MetricsViewer.breakoutLegend().within(() => {
cy.findByRole("heading", { name: /Source/ }).should("be.visible");
cy.findByText("Twitter").should("be.visible");
cy.findByText("Facebook").should("be.visible");
cy.findByText("Organic").should("be.visible");
cy.findByText("Google").should("be.visible");
cy.findByText("Affiliate").should("be.visible");
});
});
it("should add a numeric breakout dimension with default binning", () => {
cy.log("numeric breakout with default binning");
selectBreakout("Count of orders", "Total");
H.MetricsViewer.breakoutLegend().within(() => {
cy.findByRole("heading", { name: "Total" }).should("be.visible");
@@ -741,7 +710,9 @@ describe("scenarios > metrics > explorer", () => {
{ nameOrPath: "Count of orders" },
]);
cy.log("Should have 2 metric pills (expression pill is separate)");
cy.log(
"Should have 3 pills: two standalone Count of orders and an expression between them",
);
H.MetricsViewer.searchBarPills().should("have.length", 3);
cy.log(
@@ -782,7 +753,9 @@ describe("scenarios > metrics > explorer", () => {
.children()
.should("have.length.greaterThan", 1);
cy.log("Second pill should have single color (no breakout yet)");
cy.log(
"Second Count of orders pill should have single color (no breakout yet)",
);
H.MetricsViewer.searchBarPills()
.eq(2)
.findByTestId("color-indicator-container")
@@ -863,7 +836,7 @@ describe("scenarios > metrics > explorer", () => {
});
it("should preserve breakout state when editing formula and re-running", () => {
cy.log("Set up: two instances of Count of orders with an expression");
cy.log("Set up: two standalone instances of Count of orders");
addMetric("Count of orders");
H.MetricsViewer.searchBarPills().should("have.length", 2);
@@ -931,6 +904,7 @@ describe("scenarios > metrics > explorer", () => {
.eq(1)
.findByLabelText("Remove Count of orders")
.click();
H.MetricsViewer.searchBarPills().should("have.length", 2);
cy.log(
"First pill should still have breakout — multiple color indicators preserved",
@@ -941,14 +915,29 @@ describe("scenarios > metrics > explorer", () => {
.children()
.should("have.length.greaterThan", 1);
});
});
it("cannot breakout a metric math expression", () => {
describe("Expression custom names", () => {
beforeEach(() => {
H.MetricsViewer.goToViewer();
addMetric("Count of orders");
H.MetricsViewer.getMetricVisualization().should("be.visible");
});
it("should rename an expression pill without breakout options or a color change, and revert to formula text when the name is cleared", () => {
addMetricInputSequence([
{ nameOrPath: "Count of orders" },
"+",
{ nameOrPath: testMeasurePath },
]);
cy.log("Capture the expression pill color before renaming");
H.MetricsViewer.searchBarPills()
.should("have.length", 2)
.eq(1)
.should("contain.text", "Count of orders");
getPillColors(1).as("colorsBefore");
cy.log("Expression pill menu only offers Rename, no breakout options");
H.MetricsViewer.searchBarPills().eq(1).click();
H.popover().within(() => {
@@ -958,36 +947,14 @@ describe("scenarios > metrics > explorer", () => {
cy.findByText(/Add a series breakout/).should("not.exist");
cy.findByText(/Change series breakout/).should("not.exist");
cy.findByText(/Remove series breakout/).should("not.exist");
cy.findByRole("menuitem", { name: "rename icon Rename" }).click();
});
});
});
describe("Expression custom names", () => {
beforeEach(() => {
H.MetricsViewer.goToViewer();
addMetric("Count of orders");
H.MetricsViewer.getMetricVisualization().should("be.visible");
});
it("should allow setting a custom name on an expression pill", () => {
addMetricInputSequence([
{ nameOrPath: "Count of orders" },
"+",
{ nameOrPath: testMeasurePath },
]);
cy.log("Click the expression pill to open name editor");
H.MetricsViewer.searchBarPills()
.should("have.length", 2)
.eq(1)
.should("contain.text", "Count of orders");
openExpressionRename(1);
cy.log("Type a custom name");
cy.findByTestId("expression-name-input")
.should("be.focused")
.clear()
.type("My Custom Expression{enter}");
.type("My Custom Expression{enter}", { waitForAnimations: true });
cy.log("Pill should display the custom name");
H.MetricsViewer.searchBarPills()
@@ -995,6 +962,13 @@ describe("scenarios > metrics > explorer", () => {
.eq(1)
.should("contain.text", "My Custom Expression");
cy.log("Verify the pill color has not changed after renaming");
cy.get<string[]>("@colorsBefore").then((colorsBefore) => {
getPillColors(1).then((colorsAfter) => {
expect(colorsAfter).to.deep.equal(colorsBefore);
});
});
cy.log("Add breakout on the first metric to trigger the legend");
selectBreakout("Count of orders", "Source");
@@ -1015,31 +989,8 @@ describe("scenarios > metrics > explorer", () => {
cy.findByText("My Custom Expression").should("be.visible");
cy.findByText(/Test Measure/).should("not.exist");
});
});
it("should revert to formula text when custom name is cleared", () => {
addMetricInputSequence([
{ nameOrPath: "Count of orders" },
"+",
{
nameOrPath: testMeasurePath,
},
]);
cy.log("Set a custom name");
H.MetricsViewer.searchBarPills().should("have.length", 2);
openExpressionRename(1);
cy.findByTestId("expression-name-input")
.clear()
.type("Temporary Name{enter}");
H.MetricsViewer.searchBarPills()
.should("have.length", 2)
.eq(1)
.should("contain.text", "Temporary Name");
cy.log("Clear the custom name");
H.MetricsViewer.searchBarPills().should("have.length", 2);
openExpressionRename(1);
cy.findByTestId("expression-name-input").clear().type("{enter}");
@@ -1049,17 +1000,14 @@ describe("scenarios > metrics > explorer", () => {
H.MetricsViewer.searchBarPills()
.should("have.length", 2)
.eq(1)
.should("not.contain.text", "Temporary Name")
.should("not.contain.text", "My Custom Expression")
.should("contain.text", "Count of orders");
cy.log("Add breakout on the first metric to trigger the legend");
selectBreakout("Count of orders", "Source");
cy.log(
"Legend should use the formula-derived name, not the old custom name",
);
H.MetricsViewer.breakoutLegend().within(() => {
cy.findByText("Temporary Name").should("not.exist");
cy.findByText("My Custom Expression").should("not.exist");
cy.findAllByText("Count of orders + Test Measure")
.should("be.visible")
.should("have.length", 2);
@@ -1069,16 +1017,16 @@ describe("scenarios > metrics > explorer", () => {
"Tooltip should use the formula-derived name, not the old custom name",
);
H.cartesianChartCircle()
.should("have.length.at.least", 4)
.eq(4)
.should("have.length.at.least", 6)
.eq(5)
.trigger("mousemove", { force: true });
H.echartsTooltip().within(() => {
cy.findByText("Temporary Name").should("not.exist");
cy.findByText("My Custom Expression").should("not.exist");
cy.findByText("Count of orders + Test Measure").should("be.visible");
});
});
it("should preserve custom name when re-running with the same expression", () => {
it("should preserve custom name when the formula is re-run with the expression unchanged", () => {
addMetricInputSequence([
{ nameOrPath: "Count of orders" },
"+",
@@ -1097,7 +1045,7 @@ describe("scenarios > metrics > explorer", () => {
.eq(1)
.should("contain.text", "My Stable Name");
cy.log("Enter formula mode and re-run the expression");
cy.log("Append another metric and re-run the formula");
addMetric("Count of products");
cy.log("Custom name should still be preserved");
@@ -1117,35 +1065,6 @@ describe("scenarios > metrics > explorer", () => {
});
});
it("should not change expression pill color when renaming", () => {
addMetricInputSequence([
{ nameOrPath: "Count of orders" },
"+",
{ nameOrPath: testMeasurePath },
]);
cy.log("Capture the expression pill color before renaming");
const expressionPillIndex = 1;
getPillColors(expressionPillIndex).then((colorsBefore) => {
cy.log("Set a custom name on the expression pill");
H.MetricsViewer.searchBarPills().should("have.length", 2);
openExpressionRename(1);
cy.findByTestId("expression-name-input")
.clear()
.type("Renamed Expression{enter}", { waitForAnimations: true });
H.MetricsViewer.searchBarPills()
.should("have.length", 2)
.eq(1)
.should("contain.text", "Renamed Expression");
cy.log("Verify the pill color has not changed after renaming");
getPillColors(expressionPillIndex).then((colorsAfter) => {
expect(colorsAfter).to.deep.equal(colorsBefore);
});
});
});
it("should preserve custom name when the expression is edited in place but keeps at least one original metric", () => {
addMetricInputSequence([
{ nameOrPath: "Count of orders" },
@@ -1192,10 +1111,7 @@ describe("scenarios > metrics > explorer", () => {
});
it("should keep each expression's own name when an earlier expression is removed", () => {
// Regression: names used to shift up by ordinal position, so deleting
// the first expression made the second one inherit "First Name".
// Now names are bound to identities — the surviving expression keeps
// its own "Second Name".
// Names are bound to expression identities, not ordinal positions.
cy.log("Build two separately-named expressions");
addMetricInputSequence(
[
@@ -1270,12 +1186,20 @@ describe("scenarios > metrics > explorer", () => {
H.MetricsViewer.getMetricVisualization().should("be.visible");
});
it("should show all curated dimensions for a standalone metric", () => {
it("should render column labels as static text, show all curated dimensions for a standalone metric, and replace the selected one", () => {
H.MetricsViewer.getMetricVisualization().should("be.visible");
H.MetricsViewer.getColumnPickerButton()
.should("contain.text", "Time")
.and("not.contain.text", "Created At");
cy.log("column labels render as static text");
showColumnLabels();
H.MetricsViewer.getDimensionPillBarContainer().within(() => {
cy.findByText("Created At").should("be.visible").click();
cy.findByRole("button").should("not.exist");
});
H.MetricsViewer.dimensionPickerSidebar().should("not.exist");
H.MetricsViewer.openDimensionPickerSidebar().within(() => {
cy.findByRole("heading", { name: "Break out" }).should("be.visible");
cy.findByLabelText("Search fields").should("be.visible");
@@ -1293,6 +1217,41 @@ describe("scenarios > metrics > explorer", () => {
H.MetricsViewer.closeDimensionPickerSidebar();
H.MetricsViewer.dimensionPickerSidebar().should("not.exist");
H.MetricsViewer.getMetricVisualization().should("be.visible");
cy.log("replace the selected curated dimension");
H.MetricsViewer.openDimensionPickerSidebar().within(() => {
cy.findByRole("button", { name: "Category" }).should("be.visible");
cy.findByRole("button", { name: "Source" }).should("be.visible");
cy.findByRole("button", { name: "Category" }).click();
});
cy.wait("@dataset");
H.MetricsViewer.dimensionPickerSidebar().within(() => {
cy.findByRole("button", { name: "Category" })
.scrollIntoView()
.should("be.visible")
.and("have.attr", "aria-pressed", "true");
cy.findByRole("button", { name: "Source" })
.scrollIntoView()
.should("be.visible")
.and("have.attr", "aria-pressed", "false");
cy.findByRole("button", { name: "Source" }).click();
});
cy.wait("@dataset");
H.MetricsViewer.getColumnPickerButton()
.should("contain.text", "Source")
.and("not.contain.text", "Category");
H.MetricsViewer.dimensionPickerSidebar().within(() => {
cy.findByRole("button", { name: "Category" })
.scrollIntoView()
.should("be.visible")
.and("have.attr", "aria-pressed", "false");
cy.findByRole("button", { name: "Source" })
.scrollIntoView()
.should("be.visible")
.and("have.attr", "aria-pressed", "true");
});
});
it("should select and reopen the No breakout state from the viewer controls", () => {
@@ -1312,7 +1271,6 @@ describe("scenarios > metrics > explorer", () => {
);
});
H.MetricsViewer.assertVizType("Number");
H.MetricsViewer.breakoutLegend().should("not.exist");
H.MetricsViewer.closeDimensionPickerSidebar();
H.MetricsViewer.dimensionPickerSidebar().should("not.exist");
@@ -1365,42 +1323,6 @@ describe("scenarios > metrics > explorer", () => {
H.MetricsViewer.assertVizType("Line");
});
it("should replace the selected curated dimension", () => {
H.MetricsViewer.openDimensionPickerSidebar().within(() => {
cy.findByRole("button", { name: "Category" }).should("be.visible");
cy.findByRole("button", { name: "Source" }).should("be.visible");
cy.findByRole("button", { name: "Category" }).click();
});
cy.wait("@dataset");
H.MetricsViewer.dimensionPickerSidebar().within(() => {
cy.findByRole("button", { name: "Category" })
.scrollIntoView()
.should("be.visible")
.and("have.attr", "aria-pressed", "true");
cy.findByRole("button", { name: "Source" })
.scrollIntoView()
.should("be.visible")
.and("have.attr", "aria-pressed", "false");
cy.findByRole("button", { name: "Source" }).click();
});
cy.wait("@dataset");
H.MetricsViewer.getColumnPickerButton()
.should("contain.text", "Source")
.and("not.contain.text", "Category");
H.MetricsViewer.dimensionPickerSidebar().within(() => {
cy.findByRole("button", { name: "Category" })
.scrollIntoView()
.should("be.visible")
.and("have.attr", "aria-pressed", "false");
cy.findByRole("button", { name: "Source" })
.scrollIntoView()
.should("be.visible")
.and("have.attr", "aria-pressed", "true");
});
});
it("should only show shared dimensions by default for multiple metric sources", () => {
addMetric(["Our analytics", "Count of feedback"]);
verifyMetricCount(2);
@@ -1426,13 +1348,8 @@ describe("scenarios > metrics > explorer", () => {
it("should configure per-metric dimensions for a shared category", () => {
addMetric("Count of products");
H.MetricsViewer.openDimensionPickerSidebar()
.findByRole("button", { name: "Time" })
.realHover();
H.MetricsViewer.dimensionPickerSidebar()
.findByRole("button", { name: "Configure Time" })
.click();
H.MetricsViewer.openDimensionPickerSidebar();
openTimeDimensionConfiguration();
H.MetricsViewer.dimensionPickerSidebar()
.findByLabelText("Select dimension for Count of orders")
@@ -1448,38 +1365,6 @@ describe("scenarios > metrics > explorer", () => {
.should("have.value", "Created At");
});
it("should render column labels as static text", () => {
H.MetricsViewer.getMetricControls()
.findByLabelText("Column label options")
.click();
cy.findByRole("switch", { name: "Show column labels" }).click();
H.MetricsViewer.getDimensionPillBarContainer().within(() => {
cy.findByText("Created At").should("be.visible").click();
cy.findByRole("button").should("not.exist");
});
H.MetricsViewer.dimensionPickerSidebar().should("not.exist");
});
it("should auto-assign dimensions for a newly added metric after running the formula", () => {
cy.log(
"After adding a second metric, all dimension labels should have a selected dimension",
);
addMetric("Count of products");
H.MetricsViewer.getColumnPickerButton().should(
"not.contain.text",
"Select a dimension",
);
selectDimensionBreakout("Category");
H.MetricsViewer.getColumnPickerButton().should(
"not.contain.text",
"Select a dimension",
);
});
it("should preserve a selected dimension after page reload", () => {
addMetricInputSequence([
{ nameOrPath: "Count of orders" },
@@ -1525,20 +1410,17 @@ describe("scenarios > metrics > explorer", () => {
});
describe("Expression pills", () => {
it("should show an expression dimension pill with per-metric accordion", () => {
it("should preserve non-default expression dimensions after page reload", () => {
H.MetricsViewer.goToViewer();
cy.log("Create expression: Count of orders + Count of products");
addOrdersProductsExpression();
cy.log(
"Dimension pill bar should contain a selected expression dimension label",
"Create expression with only expression entity: Count of orders + Count of products",
);
addOrdersProductsExpression();
showColumnLabels();
H.MetricsViewer.getDimensionPillBarContainer()
.should("be.visible")
.and("not.contain.text", "Select dimensions");
.and("contain.text", "Multiple dimensions");
cy.log("Open the sidebar dimension picker");
H.MetricsViewer.openDimensionPickerSidebar();
cy.log(
@@ -1558,40 +1440,11 @@ describe("scenarios > metrics > explorer", () => {
.should("be.visible")
.and("have.length", 2);
});
cy.log(
"Configure the shared Time category and select a non-default dimension",
);
H.MetricsViewer.dimensionPickerSidebar()
.findByRole("button", { name: "Back" })
.click();
openTimeDimensionConfiguration();
H.MetricsViewer.dimensionPickerSidebar()
.findByLabelText("Select dimension for Count of orders")
.click();
cy.findByRole("option", { name: /Birth Date/ }).click();
cy.wait("@dataset");
cy.log(
"Expression dimension pill should now show 'Multiple dimensions'",
);
H.MetricsViewer.getDimensionPillBarContainer().should(
"contain.text",
"Multiple dimensions",
);
});
it("should preserve non-default expression dimensions after page reload", () => {
H.MetricsViewer.goToViewer();
cy.log(
"Create expression with only expression entity: Count of orders + Count of products",
);
addOrdersProductsExpression();
showColumnLabels();
cy.log("Pick a non-default dimension for one metric in the expression");
H.MetricsViewer.openDimensionPickerSidebar();
openTimeDimensionConfiguration();
H.MetricsViewer.dimensionPickerSidebar()
.findByLabelText("Select dimension for Count of orders")
@@ -1600,13 +1453,9 @@ describe("scenarios > metrics > explorer", () => {
cy.wait("@dataset");
cy.log(
"Verify the pill shows 'Multiple dimensions' (non-default state)",
);
H.MetricsViewer.getDimensionPillBarContainer().should(
"contain.text",
"Multiple dimensions",
);
H.MetricsViewer.dimensionPickerSidebar()
.findByLabelText("Select dimension for Count of orders")
.should("have.value", "Birth Date");
waitForSerializedDimensionBreakout();
cy.log("Reload the page and verify the dimension choice persists");
@@ -1639,24 +1488,12 @@ describe("scenarios > metrics > explorer", () => {
addMetric("Count of orders");
});
it("should show unified view for display types that support multiple series", () => {
addMetric("Count of products");
cy.log("line charts support multiple series, so should be unified");
H.MetricsViewer.assertVizType("Line");
H.MetricsViewer.getAllMetricVisualizations().should("have.length", 1);
cy.log("bar charts also support multiple series");
selectDimensionBreakout("Category");
H.MetricsViewer.assertVizType("Bar");
H.MetricsViewer.getAllMetricVisualizations().should("have.length", 1);
});
it("should stack series into panels when the stack series button is toggled", () => {
addMetric("Count of products");
cy.log("line chart with multiple series should show chart layout picker");
H.MetricsViewer.assertVizType("Line");
H.MetricsViewer.getAllMetricVisualizations().should("have.length", 1);
cy.findByTestId("chart-layout-picker").should("be.visible");
cy.findByLabelText("Stack layout").click();
H.expectUnstructuredSnowplowEvent({
@@ -1676,27 +1513,6 @@ describe("scenarios > metrics > explorer", () => {
H.MetricsViewer.assertVizType("Map");
cy.findByTestId("chart-layout-picker").should("not.exist");
});
it("should automatically split for display types that do not support multiple series", () => {
cy.log("with a single series, map shows one visualization");
selectDimensionBreakout("State");
H.MetricsViewer.assertVizType("Map");
H.MetricsViewer.getAllMetricVisualizations().should("have.length", 1);
cy.log("add a breakout to create multiple series");
selectDimensionBreakout("Created At", { waitForDataset: false });
selectBreakout("Count of orders", "Source");
cy.log("line supports multiple series, so should remain unified");
H.MetricsViewer.getAllMetricVisualizations().should("have.length", 1);
cy.log("map does not support multiple series, so should auto-split");
selectDimensionBreakout("State");
H.MetricsViewer.getAllMetricVisualizations().should(
"have.length.greaterThan",
1,
);
});
});
describe("Filters", () => {
@@ -1705,7 +1521,14 @@ describe("scenarios > metrics > explorer", () => {
addMetric("Count of orders");
});
it("should apply a categorical filter to a metric (UXW-4849)", () => {
it("should add, edit, and remove categorical metric and dimension filters with a series breakout, and step through them with browser history (UXW-4849)", () => {
cy.log("with a single series, map shows one visualization");
selectDimensionBreakout("State");
H.MetricsViewer.assertVizType("Map");
H.MetricsViewer.getAllMetricVisualizations().should("have.length", 1);
cy.log("add a breakout to create multiple series");
selectDimensionBreakout("Created At", { waitForDataset: false });
selectBreakout("Count of orders", "Category");
H.MetricsViewer.breakoutLegend()
.should("contain.text", "Doohickey")
@@ -1713,6 +1536,9 @@ describe("scenarios > metrics > explorer", () => {
.should("contain.text", "Gizmo")
.should("contain.text", "Widget");
cy.log("line supports multiple series, so should remain unified");
H.MetricsViewer.getAllMetricVisualizations().should("have.length", 1);
H.MetricsViewer.getFilterButton().click();
H.popover().findByText("Category").click();
@@ -1849,9 +1675,11 @@ describe("scenarios > metrics > explorer", () => {
H.MetricsViewer.getAllFilterPills().should("have.length", 0);
});
it("should allow me to apply filters to each metric individually (UXW-4849)", () => {
it("should apply filters to each metric individually, and change time granularity and date range (UXW-4849)", () => {
addMetric("Count of products");
selectDimensionBreakout("Category");
H.MetricsViewer.assertVizType("Bar");
H.MetricsViewer.getAllMetricVisualizations().should("have.length", 1);
H.MetricsViewer.changeVizType("line");
H.MetricsViewer.getMetricVisualizationDataPoints().should(
"have.length",
@@ -1993,14 +1821,12 @@ describe("scenarios > metrics > explorer", () => {
H.MetricsViewer.breakoutLegend()
.findAllByTestId("breakout-legend-dot")
.then(($dots) => {
.should(($dots) => {
expect($dots.length).to.be.lessThan(
Object.keys(colorsBefore).length,
"Filtering should reduce the number of legend items",
);
const legendHexColors: string[] = [];
$dots.each((_i, dot) => {
const $dot = Cypress.$(dot);
const color = $dot.css("background-color");
@@ -2009,8 +1835,12 @@ describe("scenarios > metrics > explorer", () => {
color,
`Color for "${label}" should be stable after filtering`,
);
legendHexColors.push(Color(color).hex());
});
})
.then(($dots) => {
const legendHexColors = $dots
.toArray()
.map((dot) => Color(Cypress.$(dot).css("background-color")).hex());
cy.log("Chart series colors should match legend colors");
for (const hex of legendHexColors) {
@@ -2133,8 +1963,14 @@ describe("scenarios > metrics > explorer", () => {
});
it("should allow me to do brush style time range filtering", () => {
H.MetricsViewer.getMetricControls()
.findByRole("button", { name: /All time/i })
.should("be.visible");
H.ensureChartIsActive();
H.applyBrush(100, 250);
H.MetricsViewer.getMetricControls()
.findByRole("button", { name: /All time/i })
.should("not.exist");
H.MetricsViewer.getMetricVisualization().within(() => {
cy.findByText(/June/).should("be.visible");
cy.findByText(/July/).should("be.visible");
@@ -2146,9 +1982,8 @@ describe("scenarios > metrics > explorer", () => {
});
});
describe("Dimension filters", () => {
describe("Expression editing", () => {
beforeEach(() => {
interceptDatasetQuery();
H.MetricsViewer.goToViewer();
});
@@ -2181,7 +2016,6 @@ describe("scenarios > metrics > explorer", () => {
describe("Responsive viewer controls", () => {
const setupTimeControls = (width: number) => {
cy.viewport(1280, 900);
interceptDatasetQuery();
H.MetricsViewer.goToViewer();
H.MetricsViewer.searchInput().type("{end}, Count of orders", {
waitForAnimations: true,
@@ -2231,7 +2065,6 @@ describe("scenarios > metrics > explorer", () => {
describe("Metric math", () => {
beforeEach(() => {
interceptDatasetQuery();
H.MetricsViewer.goToViewer();
addMetric("Count of orders");
});
@@ -2289,13 +2122,16 @@ describe("scenarios > metrics > explorer", () => {
.findByText("2")
.should("be.visible");
cy.log("dimension filter is applied");
H.MetricsViewer.getMetricVisualizationDataPoints().should(
"have.length.of.at.most",
60,
);
cy.log("metric math expression still renders with the applied state");
H.MetricsViewer.getMetricVisualization().should("be.visible");
cy.log("the Category filters are applied");
H.echartsContainer().within(() => {
cy.findByText("Doohickey").should("be.visible");
cy.findByText("Gadget").should("not.exist");
cy.findByText("Gizmo").should("not.exist");
cy.findByText("Widget").should("not.exist");
});
}
assertMetricMath();
@@ -2340,12 +2176,13 @@ describe("scenarios > metrics > explorer", () => {
cy.log(
"Append literal number 123 — typed without selecting from dropdown",
);
addMetricInputSequence([
"+",
{ nameOrPath: ["Our analytics", NUMERIC_METRIC_NAME] },
]);
addMetricInputSequence(["+ 123"]);
H.MetricsViewer.getMetricVisualization().should("be.visible");
cy.log("2 × 18,760 orders + the literal 123");
H.MetricsViewer.getMetricVisualization()
.should("be.visible")
.and("contain.text", "123 + 123 + 123")
.and("contain.text", "37,643");
cy.log("Append metric '123' as standalone — selected from dropdown");
addMetricInputSequence([
@@ -2356,7 +2193,9 @@ describe("scenarios > metrics > explorer", () => {
cy.log("Verify final pill layout");
H.MetricsViewer.searchBarPills().should("have.length", 2);
H.MetricsViewer.searchBarPills().eq(0).should("contain", "123 + 123");
H.MetricsViewer.getAllMetricVisualizations()
.eq(0)
.should("contain.text", "37,643");
H.MetricsViewer.searchBarPills().eq(1).should("contain", "123");
});
});
@@ -2586,7 +2425,21 @@ describe("scenarios > metrics > explorer > shared dimensions", () => {
it("does not share same-named dimensions from different source columns", () => {
visitViewerWithMetrics(["People with renamed source", "Plain products"]);
H.MetricsViewer.openDimensionPickerSidebar().within(() => {
H.MetricsViewer.openDimensionPickerSidebar();
H.MetricsViewer.dimensionPickerSidebar()
.findByRole("button", { name: "See all" })
.click();
getExpandedMetricAccordion("People with renamed source")
.findByRole("button", { name: "Category" })
.should("be.visible");
getExpandedMetricAccordion("Plain products")
.findByRole("button", { name: "Category" })
.should("be.visible");
H.MetricsViewer.dimensionPickerSidebar()
.findByRole("button", { name: "Back" })
.click();
H.MetricsViewer.dimensionPickerSidebar().within(() => {
cy.findByRole("button", { name: "Time" }).should("be.visible");
cy.log(
"People's renamed Source and Products' Category share a name but not a source column",
@@ -2598,7 +2451,21 @@ describe("scenarios > metrics > explorer > shared dimensions", () => {
it("does not mix country and state geo dimensions", () => {
visitViewerWithMetrics(["Accounts by country", "People by state"]);
H.MetricsViewer.openDimensionPickerSidebar().within(() => {
H.MetricsViewer.openDimensionPickerSidebar();
H.MetricsViewer.dimensionPickerSidebar()
.findByRole("button", { name: "See all" })
.click();
getExpandedMetricAccordion("Accounts by country")
.findByRole("button", { name: "Country" })
.should("be.visible");
getExpandedMetricAccordion("People by state")
.findByRole("button", { name: "State" })
.should("be.visible");
H.MetricsViewer.dimensionPickerSidebar()
.findByRole("button", { name: "Back" })
.click();
H.MetricsViewer.dimensionPickerSidebar().within(() => {
cy.findByRole("button", { name: "Time" }).should("be.visible");
cy.findByRole("button", { name: "Country" }).should("not.exist");
cy.findByRole("button", { name: "State" }).should("not.exist");
@@ -2679,7 +2546,7 @@ describe("scenarios > metrics > explorer > shared dimensions", () => {
.should("contain.text", "Category");
});
it("disables metrics without a dimension of the picked type", () => {
it("leaves metrics without a dimension of the picked type unmapped", () => {
visitViewerWithMetrics(["Plain orders", "Plain products"]);
H.MetricsViewer.openDimensionPickerSidebar();
@@ -2692,7 +2559,7 @@ describe("scenarios > metrics > explorer > shared dimensions", () => {
.click();
cy.wait("@dataset");
cy.log("Orders has no category dimensions, so it is excluded");
cy.log("Orders has no category dimensions, so it is left unmapped");
assertSerializedDimensionBreakout((dimensionBreakout) => {
expect(dimensionBreakout.t).to.equal("category");
expect(dimensionBreakout.D?.find((entry) => entry.i === 0)?.d).to.be
@@ -2843,7 +2710,5 @@ function createTestMeasure(
aggregation: [aggregation],
},
},
}).then(({ body }) => {
cy.wrap(body.id).as("measureId");
});
}

View File

@@ -111,27 +111,12 @@ describe("scenarios > metrics > question", () => {
H.echartsContainer().findByText("Expression").should("be.visible");
});
it("should be able to add a breakout to a question that uses a metric", () => {
it("should be able to change the temporal unit and then replace the breakout when consuming a timeseries metric", () => {
H.createQuestion(ORDERS_TIMESERIES_METRIC).then(({ body: metric }) => {
createQuestionWithMetric(metric.id, { breakout: MONTH_BREAKOUT });
});
H.openNotebook();
H.getNotebookStep("summarize")
.findByTestId("breakout-step")
.findByText("Created At: Month")
.click();
H.popover().within(() => {
cy.findByText("Product").click();
cy.findByText("Category").click();
});
H.visualize();
H.echartsContainer().findByText("Product → Category").should("be.visible");
});
it("should be able to change the temporal unit when consuming a timeseries metric", () => {
H.createQuestion(ORDERS_TIMESERIES_METRIC).then(({ body: metric }) => {
createQuestionWithMetric(metric.id, { breakout: MONTH_BREAKOUT });
});
cy.log("change the temporal unit");
H.openNotebook();
H.getNotebookStep("summarize")
.findByTestId("breakout-step")
@@ -144,12 +129,27 @@ describe("scenarios > metrics > question", () => {
});
H.visualize();
H.assertQueryBuilderRowCount(5);
cy.log("replace the breakout");
H.openNotebook();
H.getNotebookStep("summarize")
.findByTestId("breakout-step")
.findByText("Created At: Year")
.click();
H.popover().within(() => {
cy.findByText("Product").click();
cy.findByText("Category").click();
});
H.visualize();
H.echartsContainer().findByText("Product → Category").should("be.visible");
});
it("should be able to drill-thru with a metric", () => {
it("should be able to drill-thru with a metric by breaking out the point and by viewing its underlying records", () => {
H.createQuestion(ORDERS_TIMESERIES_METRIC).then(({ body: metric }) => {
createQuestionWithMetric(metric.id, { breakout: MONTH_BREAKOUT });
});
cy.log("break out the point by another dimension");
H.cartesianChartCircle().eq(23).click({ force: true });
H.popover().within(() => {
cy.findByText("Break out by…").click();
@@ -158,12 +158,13 @@ describe("scenarios > metrics > question", () => {
});
cy.wait("@dataset");
H.echartsContainer().findByText("User → Source").should("be.visible");
});
it("should be able to drill-thru with a metric without the aggregation clause", () => {
H.createQuestion(ORDERS_TIMESERIES_METRIC).then(({ body: metric }) => {
createQuestionWithMetric(metric.id, { breakout: MONTH_BREAKOUT });
});
cy.log("go back to the original chart");
cy.go("back");
H.echartsContainer().findByText("User → Source").should("not.exist");
H.echartsContainer().findByText("Created At: Month").should("be.visible");
cy.log("see the underlying records of the point");
H.cartesianChartCircle().eq(23).click({ force: true });
H.popover().findByText("See these Orders").click();
cy.wait("@dataset");
@@ -184,58 +185,5 @@ describe("scenarios > metrics > question", () => {
.should("be.visible")
.and("have.attr", "data-viz-ui-name", "Number");
cy.findByTestId("scalar-value").should("be.visible");
H.MetricPage.aboutPage().within(() => {
cy.button(/Filter/).should("not.exist");
cy.button(/Summarize/).should("not.exist");
});
});
});
describe("metrics", () => {
beforeEach(() => {
H.resetSnowplow();
H.restore();
cy.signInAsAdmin();
H.enableTracking();
});
afterEach(() => {
H.expectNoBadSnowplowEvents();
});
it("should bookmark a metric", () => {
H.createQuestion({ ...ORDERS_SCALAR_METRIC, name: "Metric Foo" });
H.createQuestion({ ...ORDERS_SCALAR_METRIC, name: "Metric Bar" });
H.createQuestion(
{ ...ORDERS_SCALAR_METRIC, name: "Metric Baz" },
{ visitQuestion: true },
);
H.MetricPage.moreMenu().click();
H.popover().findByTextEnsureVisible("Bookmark").click();
H.navigationSidebar().findByText("Metric Baz").should("be.visible");
H.navigationSidebar().findByText("Our analytics").click();
cy.findAllByTestId("collection-entry")
.filter(":contains(Metric Bar)")
.icon("ellipsis")
.click();
H.popover().findByText("Bookmark").click();
H.expectUnstructuredSnowplowEvent({
event: "bookmark_added",
event_detail: "metric",
triggered_from: "collection_list",
});
H.navigationSidebar().findByText("Metrics").click();
cy.findAllByRole("row")
.filter(":contains(Metric Foo)")
.icon("ellipsis")
.click();
H.popover().findByText("Bookmark").click();
H.expectUnstructuredSnowplowEvent({
event: "bookmark_added",
event_detail: "metric",
triggered_from: "browse_metrics",
});
});
});

View File

@@ -17,26 +17,24 @@ describe("scenarios > metrics > search", () => {
beforeEach(() => {
H.restore();
cy.signInAsNormalUser();
cy.intercept("POST", "/api/dataset").as("dataset");
cy.intercept("POST", "/api/metric/dataset").as("metricDataset");
cy.intercept("GET", "/api/search?q=*").as("search");
});
it("should be able to search for metrics in global search", () => {
it("should be able to search for metrics in global search and on the search page", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
cy.log("command palette");
cy.visit("/");
H.commandPaletteSearch(ORDERS_SCALAR_METRIC.name, false);
H.commandPalette()
.findByRole("option", { name: ORDERS_SCALAR_METRIC.name })
.click();
H.MetricPage.aboutPage().should("be.visible");
});
it("should be able to search for metrics on the search page", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
cy.log("search page");
cy.intercept("GET", "/api/search?*context=search-app*").as("searchPage");
cy.visit("/");
H.commandPaletteSearch(ORDERS_SCALAR_METRIC.name, true);
cy.wait("@search");
cy.wait("@searchPage");
cy.findByTestId("search-app").within(() => {
cy.findByText(ORDERS_SCALAR_METRIC.name).should("be.visible");
cy.findByTestId("type-search-filter").click();
@@ -45,23 +43,11 @@ describe("scenarios > metrics > search", () => {
cy.findByText("Metric").click();
cy.findByText("Apply").click();
});
cy.wait("@search");
cy.wait("@searchPage");
cy.findByTestId("search-app").within(() => {
cy.findByText("1 result").should("be.visible");
cy.findByText(ORDERS_SCALAR_METRIC.name).click();
});
H.MetricPage.aboutPage().should("be.visible");
});
it("should see metrics in recent items in global search", () => {
H.createQuestion(ORDERS_SCALAR_METRIC).then(({ body: card }) => {
H.visitMetric(card.id);
});
cy.visit("/");
H.commandPaletteSearch(ORDERS_SCALAR_METRIC.name, false);
H.commandPalette()
.findByRole("option", { name: ORDERS_SCALAR_METRIC.name })
.click();
H.MetricPage.aboutPage().should("be.visible");
});
});

View File

@@ -43,13 +43,14 @@ describe("issue 47058", () => {
cy.findByText("Loading...").should("be.visible");
H.getNotebookStep("summarize").should("not.exist");
cy.findByText("[Unknown Metric]").should("not.exist");
cy.wait("@metadata");
cy.findByText("Loading...").should("not.exist");
H.getNotebookStep("summarize").should("be.visible");
H.getNotebookStep("summarize")
.findByText("Metric 47058")
.should("be.visible");
cy.findByText("[Unknown Metric]").should("not.exist");
});
});
@@ -107,6 +108,7 @@ describe("issue 44171", () => {
it("should not save viz settings on metrics", () => {
cy.intercept("PUT", "/api/card/*").as("saveCard");
cy.intercept("POST", "/api/dataset").as("dataset");
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
cy.get<number>("@metricBId").then((metricBId) => {
@@ -119,6 +121,8 @@ describe("issue 44171", () => {
cy.findByText("Sum of ...").click();
cy.findByText("Total").click();
});
H.runButtonInOverlay().click();
cy.wait("@dataset");
H.MetricPage.saveButton().click();
cy.wait("@saveCard");
@@ -147,60 +151,6 @@ describe("issue 44171", () => {
});
});
describe("issue 32037", () => {
beforeEach(() => {
H.restore();
cy.signInAsNormalUser();
H.createQuestion(
{
name: "Metric 32037",
type: "metric",
display: "line",
query: {
"source-table": ORDERS_ID,
aggregation: [["count"]],
breakout: [
[
"field",
ORDERS.CREATED_AT,
{ "temporal-unit": "month", "base-type": "type/DateTime" },
],
],
},
},
{ wrapId: true, idAlias: "metricId" },
);
});
it("should show unsaved changes modal and allow to discard changes when editing a metric (metabase#32037)", () => {
cy.get<number>("@metricId").then((metricId) => {
cy.visit(`/metric/${metricId}/query`);
});
H.MetricPage.queryEditor().should("be.visible");
H.MetricPage.saveButton().should("not.exist");
H.getNotebookStep("summarize").findByText("Count").click();
H.popover().within(() => {
cy.findByText("Sum of ...").click();
cy.findByText("Total").click();
});
H.MetricPage.saveButton().should("be.visible");
H.MetricPage.aboutTab().click();
H.modal().within(() => {
cy.findByText("Discard your changes?").should("be.visible");
cy.findByText("Discard changes").click();
});
H.MetricPage.aboutPage().should("be.visible");
cy.get<number>("@metricId").then((metricId) => {
cy.location("pathname").should("eq", `/metric/${metricId}`);
});
});
});
describe("issue 79571", () => {
const METRIC_NAME = "Metric 79571";
@@ -218,7 +168,7 @@ describe("issue 79571", () => {
cy.signInAsNormalUser();
});
it("logs choosing a metric as a recent selection and lists it under Recent items (metabase#79571)", () => {
it("logs choosing a metric as a recent selection, offers custom column and join actions on it, and lists it under Recent items (metabase#79571)", () => {
H.createQuestion(ORDERS_COUNT_METRIC).then(({ body: { id: metricId } }) => {
cy.intercept("POST", "/api/activity/recents").as("logRecent");
@@ -237,6 +187,15 @@ describe("issue 79571", () => {
expect(response?.statusCode).to.eq(204);
});
H.getNotebookStep("data").within(() => {
cy.findByTestId("action-buttons")
.button("Custom column")
.should("be.visible");
cy.findByTestId("action-buttons")
.button("Join data")
.should("be.visible");
});
// Reopening the picker now surfaces the metric under Recent items
H.getNotebookStep("data").findByText("Orders").click();
H.miniPickerHeader().click();