Skip to content
Snippets Groups Projects
Unverified Commit 60e03e8a authored by Kamil Mielnik's avatar Kamil Mielnik Committed by GitHub
Browse files

Refactor `cy.archiveQuestion`, `cy.createQuestion` & `cy.createNativeQuestion`...

Refactor `cy.archiveQuestion`, `cy.createQuestion` & `cy.createNativeQuestion` to TypeScript (#39614) (#40282)

* Convert commands/api/question to TS

* Add missing embedding_params and collection_position attributes to Card

* Type question function

* Move types to the top of file

* Inline throwIfNotPresent

* Add types for logAction

* Define StructuredQuestionDetails & NativeQuestionDetails

* Add typing to commands

* Return for consistency

* Remove queryType argument from question

* Define typing for archiveQuestion command

* Format code

* Define typing for createQuestion and createNativeQuestion commands

* Make options optional

* Remove TODO

* Use arrow functions, update JSDoc

* Use type for consistency

* Add missing attributes in mocks

* Make embedding_params non-optional
parent bc4576a2
Branches
Tags
No related merge requests found
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import type {
Card,
DatasetQuery,
NativeQuery,
StructuredQuery,
} from "metabase-types/api";
Cypress.Commands.add("createQuestion", (questionDetails, customOptions) => {
const { name, query } = questionDetails;
declare global {
namespace Cypress {
interface Chainable {
archiveQuestion(
id: Card["id"],
): Cypress.Chainable<Cypress.Response<Card>>;
createQuestion(
questionDetails: StructuredQuestionDetails,
options?: Options,
): Cypress.Chainable<Cypress.Response<Card>>;
createNativeQuestion(
questionDetails: NativeQuestionDetails,
options?: Options,
): Cypress.Chainable<Cypress.Response<Card>>;
}
}
}
throwIfNotPresent(query);
type QuestionDetails = {
dataset_query: DatasetQuery;
/**
* Defaults to "test question".
*/
name?: Card["name"];
description?: Card["description"];
/**
* Entity type.
* Defaults to "question".
*/
type?: Card["type"];
/**
* Defaults to "table".
*/
display?: Card["display"];
parameters?: Card["parameters"];
visualization_settings?: Card["visualization_settings"];
/**
* Parent collection in which to store this question.
*/
collection_id?: Card["collection_id"];
/**
* Used on the frontend to determine whether the question is pinned or not.
*/
collection_position?: Card["collection_position"];
embedding_params?: Card["embedding_params"];
/**
* Defaults to false.
*/
enable_embedding?: Card["enable_embedding"];
};
logAction("Create a QB question", name);
return question("query", questionDetails, customOptions);
});
type StructuredQuestionDetails = Omit<QuestionDetails, "dataset_query"> & {
/**
* Defaults to SAMPLE_DB_ID.
*/
database?: DatasetQuery["database"];
query: StructuredQuery;
};
Cypress.Commands.add("archiveQuestion", id => {
type NativeQuestionDetails = Omit<QuestionDetails, "dataset_query"> & {
/**
* Defaults to SAMPLE_DB_ID.
*/
database?: DatasetQuery["database"];
native: NativeQuery;
};
type Options = {
/**
* Whether to visit the question in order to load its metadata.
* Defaults to false.
*/
loadMetadata?: boolean;
/**
* Whether to visit the question after the creation or not.
* Defaults to false.
*/
visitQuestion?: boolean;
/**
* Whether to wrap a question id, to make it available outside of this scope.
* Defaults to false.
*/
wrapId?: boolean;
/**
* Alias a question id in order to use it later with `cy.get("@" + alias).
* Defaults to "questionId".
*/
idAlias?: string;
/**
* We need distinctive endpoint aliases for cases where we have multiple questions or nested questions.
* Defaults to "cardQuery".
*/
interceptAlias?: string;
};
Cypress.Commands.add("archiveQuestion", (id: Card["id"]) => {
cy.log(`Archiving a question with id: ${id}`);
return cy.request("PUT", `/api/card/${id}`, {
archived: true,
});
});
Cypress.Commands.add(
"createQuestion",
(questionDetails: StructuredQuestionDetails, options?: Options) => {
const { database = SAMPLE_DB_ID, name, query } = questionDetails;
if (!query) {
throw new Error('"query" attribute missing in questionDetails');
}
logAction("Create a QB question", name);
return question(
{
...questionDetails,
dataset_query: { type: "query", query, database },
},
options,
);
},
);
Cypress.Commands.add(
"createNativeQuestion",
(questionDetails, customOptions) => {
const { name, native } = questionDetails;
(questionDetails: NativeQuestionDetails, options?: Options) => {
const { database = SAMPLE_DB_ID, name, native } = questionDetails;
throwIfNotPresent(native);
if (!native) {
throw new Error('"native" attribute missing in questionDetails');
}
logAction("Create a native question", name);
question("native", questionDetails, customOptions);
return question(
{
...questionDetails,
dataset_query: { type: "native", native, database },
},
options,
);
},
);
/**
*
* @param {("query"|"native")} queryType
*
* @param {object} questionDetails
* @param {string} [questionDetails.name="test question"]
* @param {string} questionDetails.description
* @param {("question"|"model")} questionDetails.type Entity type
* @param {object} questionDetails.native
* @param {object} questionDetails.query
* @param {number} [questionDetails.database=1]
* @param {string} [questionDetails.display="table"]
* @param {object} [questionDetails.visualization_settings={}]
* @param {number} [questionDetails.collection_id] - Parent collection in which to store this question.
* @param {number} [questionDetails.collection_position] - used on the frontend to determine whether the question is pinned or not.
*
* @param {object} customOptions
* @param {boolean} customOptions.loadMetadata - Whether to visit the question in order to load its metadata.
* @param {boolean} customOptions.visitQuestion - Whether to visit the question after the creation or not.
* @param {boolean} customOptions.wrapId - Whether to wrap a question id, to make it available outside of this scope.
* @param {string} customOptions.idAlias - Alias a question id in order to use it later with `cy.get("@" + alias).
* @param {string} customOptions.interceptAlias - We need distinctive endpoint aliases for cases where we have multiple questions or nested questions.
*/
function question(
queryType,
const question = (
{
name = "test question",
description,
dataset_query,
type = "question",
native,
query,
database = SAMPLE_DB_ID,
display = "table",
parameters,
visualization_settings = {},
......@@ -67,24 +164,20 @@ function question(
collection_position,
embedding_params,
enable_embedding = false,
} = {},
}: QuestionDetails,
{
loadMetadata = false,
visitQuestion = false,
wrapId = false,
idAlias = "questionId",
interceptAlias = "cardQuery",
} = {},
) {
}: Options = {},
) => {
return cy
.request("POST", "/api/card", {
name,
description,
dataset_query: {
type: queryType,
[queryType]: queryType === "native" ? native : query,
database,
},
dataset_query,
display,
parameters,
visualization_settings,
......@@ -114,7 +207,7 @@ function question(
if (loadMetadata || visitQuestion) {
if (type === "model") {
cy.intercept("POST", `/api/dataset`).as("dataset");
cy.intercept("POST", "/api/dataset").as("dataset");
cy.visit(`/model/${body.id}`);
cy.wait("@dataset"); // Wait for `result_metadata` to load
} else {
......@@ -127,22 +220,20 @@ function question(
}
}
});
}
};
function throwIfNotPresent(param) {
if (!param) {
throw new Error(`Wrong key! Expected "query" or "native".`);
}
}
/**
*
* @param {string} title - A title used to log the Cypress action/request that follows it.
* @param {string} [questionName] - Optional question name.
*/
function logAction(title, questionName) {
const logAction = (
/**
* A title used to log the Cypress action/request that follows it.
*/
title: string,
/**
* Optional question name.
*/
questionName?: string,
) => {
const fullTitle = `${title}: ${questionName}`;
const message = questionName ? fullTitle : title;
cy.log(message);
}
};
import type { EmbeddingParameters } from "metabase/public/lib/types";
import type { Collection } from "./collection";
import type { DashboardId, DashCardId } from "./dashboard";
import type { DatabaseId } from "./database";
......@@ -30,12 +32,14 @@ export interface Card<Q extends DatasetQuery = DatasetQuery>
/* Indicates whether static embedding for this card has been published */
enable_embedding: boolean;
embedding_params: EmbeddingParameters | null;
can_write: boolean;
initially_published_at: string | null;
database_id?: DatabaseId;
collection?: Collection | null;
collection_id: number | null;
collection_position: number | null;
result_metadata: Field[];
moderation_reviews?: ModerationReview[];
......
......@@ -32,11 +32,13 @@ export const createMockCard = (opts?: Partial<Card>): Card => ({
cache_ttl: null,
collection: null,
collection_id: null,
collection_position: null,
last_query_start: null,
average_query_time: null,
based_on_upload: null,
archived: false,
enable_embedding: false,
embedding_params: null,
initially_published_at: null,
...opts,
});
......
......@@ -70,12 +70,14 @@ function convertActionToQuestionCard(
can_write: true,
public_uuid: null,
collection_id: null,
collection_position: null,
result_metadata: [],
cache_ttl: null,
last_query_start: null,
average_query_time: null,
archived: false,
enable_embedding: false,
embedding_params: null,
initially_published_at: null,
};
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment