Skip to content
Snippets Groups Projects
Unverified Commit 8bf73a6d authored by Nemanja Glumac's avatar Nemanja Glumac Committed by GitHub
Browse files

Cypress TypeScript PoC (#36474)

* Add a simple Cypress TS helper example

* Convert simple custom Cypress commands to Ts

* Convert a bit more complex set of helpers to Ts

* Make selectors more resilient

This solves linter errors.

* Fix tests

* Type response as unknown

* Reuse FE types

* Improve JSDoc comments

* Use generic string type for aggregation metrics

* Use camelCase

* Use `CyHttpMessages.IncomingResponse` to type a response
parent e7165b0f
Branches
Tags
No related merge requests found
......@@ -3,7 +3,8 @@
"no-unscoped-text-selectors": 2,
"import/no-commonjs": 0,
"no-color-literals": 0,
"no-console": 0
"no-console": 0,
"@typescript-eslint/no-namespace": "off"
},
"env": {
"cypress/globals": true,
......
Cypress.Commands.add(
"button",
{
prevSubject: "optional",
},
(subject, button_name, timeout) => {
const config = {
name: button_name,
timeout,
};
if (subject) {
cy.wrap(subject).findByRole("button", config);
} else {
cy.findByRole("button", config);
}
},
);
declare global {
namespace Cypress {
interface Chainable {
/**
* Get a button either unscoped, or chained to a previously yielded subject.
* Uses `findByRole` under the hood.
*
* @example
* cy.button("Save").click();
* modal().button("Save").click();
*/
button(
buttonName: string,
timeout?: number,
): Cypress.Chainable<JQuery<HTMLElement>>;
}
}
}
Cypress.Commands.add(
"button",
{
prevSubject: "optional",
},
(subject, buttonName, timeout) => {
const config = {
name: buttonName,
timeout,
};
return subject
? cy.wrap(subject).findByRole("button", config)
: cy.findByRole("button", config);
},
);
export {};
Cypress.Commands.add(
"icon",
{
prevSubject: "optional",
},
(subject, icon_name) => {
const SELECTOR = `.Icon-${icon_name}`;
if (subject) {
cy.wrap(subject).find(SELECTOR);
} else {
cy.get(SELECTOR);
}
},
);
declare global {
namespace Cypress {
interface Chainable {
/**
* Get an icon either unscoped, or chained to a previously yielded subject.
* Uses jQuery under the hood.
*
* @example
* cy.icon("bolt_filled").should("have.length", 4);
* cy.findByTestId("app-bar").icon("add").click()
*/
icon(iconName: string): Cypress.Chainable<JQuery<HTMLElement>>;
}
}
}
Cypress.Commands.add(
"icon",
{
prevSubject: "optional",
},
(subject, iconName) => {
const SELECTOR = `.Icon-${iconName}`;
return subject ? cy.wrap(subject).find(SELECTOR) : cy.get(SELECTOR);
},
);
export {};
export function remapDisplayValueToFK({ display_value, name, fk } = {}) {
// Both display_value and fk are expected to be field IDs
// You can get them from e2e/support/cypress_sample_database.json
cy.request("POST", `/api/field/${display_value}/dimension`, {
field_id: display_value,
name,
human_readable_field_id: fk,
type: "external",
});
}
/**
* API helper.
* @see {@link https://www.metabase.com/docs/latest/api/field#post-apifieldiddimension API Documentation}
*
* @summary Remap a field display value to a foreign key using Metabase API.
*
* Both `display_value` and `fk` are expected to be field IDs.
* You can get them from `e2e/support/cypress_sample_database.json`
*
* @example
* remapDisplayValueToFK({
* display_value: ORDERS.PRODUCT_ID,
* name: "Product ID",
* fk: PRODUCTS.TITLE,
* });
*
*/
export function remapDisplayValueToFK({
display_value,
name,
fk,
}: {
display_value: number;
name: string;
fk: number;
}): Cypress.Chainable<Cypress.Response<unknown>> {
return cy.request("POST", `/api/field/${display_value}/dimension`, {
field_id: display_value,
name,
human_readable_field_id: fk,
type: "external",
});
}
import type { CyHttpMessages } from "cypress/types/net-stubbing";
import { popover } from "e2e/support/helpers/e2e-ui-elements-helpers";
import type { NotebookStepType } from "metabase/query_builder/components/notebook/types";
/**
* Switch to a notebook editor from a simple query view (aka "chill mode").
*/
export function openNotebook() {
return cy.icon("notebook").click();
return cy.findByTestId("qb-header-action-panel").icon("notebook").click();
}
/**
* Helps to select specific notebook steps like filters, joins, break outs, etc.
* Select a specific notebook step like filter, join, breakout, etc.
*
* Details:
* https://github.com/metabase/metabase/pull/17708#discussion_r700082403
*
* @param {string} type — notebook step type (filter, join, expression, summarize, etc.)
* @param {{stage: number; index: number}} positionConfig - indexes specifying step's position
* @returns
* @see {@link https://github.com/metabase/metabase/pull/17708#discussion_r700082403}
*/
export function getNotebookStep(type, { stage = 0, index = 0 } = {}) {
export function getNotebookStep(
type: Exclude<NotebookStepType, "aggregate" | "breakout">,
{ stage = 0, index = 0 } = {},
): Cypress.Chainable<JQuery<HTMLElement>> {
return cy.findByTestId(`step-${type}-${stage}-${index}`);
}
/**
* Visualize notebook query results.
* @summary Visualize notebook query results.
*
* This helper intelligently waits for the query to load, and gives you an option
* to assert on the waited query response.
*
* @param {function} callback
* @example
* visualize();
*
* visualize(response => {
* expect(response.body.error).to.not.exist;
* });
*/
export function visualize(callback) {
export function visualize(
callback?: (response?: CyHttpMessages.IncomingResponse) => void,
) {
cy.intercept("POST", "/api/dataset").as("dataset");
cy.button("Visualize").click();
......@@ -35,12 +48,23 @@ export function visualize(callback) {
});
}
/**
* Summarize (Aggregate).
*
* Doesn't support summarizing using Custom Expression or Common Metrics!
*/
export function addSummaryField({
metric,
table,
field,
stage = 0,
index = 0,
}: {
metric: string;
table?: string;
field?: string;
stage?: number;
index?: number;
}) {
getNotebookStep("summarize", { stage, index })
.findByTestId("aggregate-step")
......@@ -59,11 +83,19 @@ export function addSummaryField({
});
}
/**
* Breakout (Group by in the UI).
*/
export function addSummaryGroupingField({
table,
field,
stage = 0,
index = 0,
}: {
table?: string;
field: string;
stage?: number;
index?: number;
}) {
getNotebookStep("summarize", { stage, index })
.findByTestId("breakout-step")
......@@ -79,7 +111,18 @@ export function addSummaryGroupingField({
});
}
export function removeSummaryGroupingField({ field, stage = 0, index = 0 }) {
/**
* Remove breakout.
*/
export function removeSummaryGroupingField({
field,
stage = 0,
index = 0,
}: {
field: string;
stage: number;
index: number;
}) {
getNotebookStep("summarize", { stage, index })
.findByTestId("breakout-step")
.findByText(field)
......@@ -88,16 +131,17 @@ export function removeSummaryGroupingField({ field, stage = 0, index = 0 }) {
}
/**
* Joins a raw table given a table and optional LHS and RHS column names
* (for cases when join condition can't be selected automatically)
* Join a raw table given a table and optional LHS and RHS column names
* (for cases when join condition can't be selected automatically).
*
* Expects a join popover to be open
* Expects a join popover to be open!
*
* @param {string} tableName
* @param {string} [lhsColumnName]
* @param {string} [rhsColumnName]
*/
export function joinTable(tableName, lhsColumnName, rhsColumnName) {
export function joinTable(
tableName: string,
lhsColumnName?: string,
rhsColumnName?: string,
) {
popover().findByText(tableName).click();
if (lhsColumnName && rhsColumnName) {
popover().findByText(lhsColumnName).click();
......@@ -105,24 +149,41 @@ export function joinTable(tableName, lhsColumnName, rhsColumnName) {
}
}
/**
* Open a saved question and join it with another saved question.
*
* Depends on a `startNewQuestion()` helper to work properly!
*
* @todo Either decouple this dependency or use `startNewQuestion()` directly here.
*
* @example
* startNewQuestion();
* selectSavedQuestionsToJoin("Q1", "Q2");
*/
export function selectSavedQuestionsToJoin(
firstQuestionName,
secondQuestionName,
firstQuestionName: string,
secondQuestionName: string,
) {
cy.intercept("GET", "/api/database/*/schemas").as("loadSchemas");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Saved Questions").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(firstQuestionName).click();
cy.findAllByTestId("data-bucket-list-item")
.contains("Saved Questions")
.click();
cy.findByTestId("select-list")
.findAllByRole("menuitem")
.contains(firstQuestionName)
.click();
cy.wait("@loadSchemas");
// join to question b
cy.icon("join_left_outer").click();
popover().within(() => {
cy.findByTextEnsureVisible("Sample Database").click();
cy.findByTextEnsureVisible("Raw Data").click();
cy.findByTextEnsureVisible("Saved Questions").click();
cy.findByText("Sample Database").should("be.visible").click();
cy.findByText("Raw Data").should("be.visible").click();
cy.findAllByTestId("data-bucket-list-item")
.contains("Saved Questions")
.click();
cy.findByText(secondQuestionName).click();
});
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment