* Add xAI (Grok) as a Metabot provider * Scroll the provider buttons into view in the setup e2e test * Turn off reasoning on Grok 4.3 for structured output * Report reasoning support for Grok 4.3 * Add xAI to the shared LLM test connections
1014 lines
31 KiB
TypeScript
1014 lines
31 KiB
TypeScript
const { H } = cy;
|
|
const IS_ENTERPRISE = Cypress.expose("IS_ENTERPRISE");
|
|
import { USERS } from "e2e/support/cypress_data";
|
|
import { SUBSCRIBE_URL } from "metabase/setup/constants";
|
|
|
|
const { admin } = USERS;
|
|
|
|
// we're testing for one known (en) and one unknown (xx) locale
|
|
const locales = ["en", "xx"];
|
|
|
|
describe("scenarios > setup", () => {
|
|
beforeEach(() => {
|
|
H.restore("blank");
|
|
H.resetSnowplow();
|
|
});
|
|
|
|
locales.forEach((locale) => {
|
|
it(
|
|
`should allow you to sign up using "${locale}" browser locale`,
|
|
{ tags: ["@external"] },
|
|
() => {
|
|
// initial redirection and welcome page
|
|
cy.visit("/", {
|
|
// set the browser language as per:
|
|
// https://glebbahmutov.com/blog/cypress-tips-and-tricks/index.html#control-navigatorlanguage
|
|
onBeforeLoad(win) {
|
|
Object.defineProperty(win.navigator, "language", {
|
|
value: locale,
|
|
});
|
|
},
|
|
});
|
|
cy.location("pathname").should("eq", "/setup");
|
|
|
|
skipWelcomePage();
|
|
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
// ====
|
|
// User
|
|
// ====
|
|
|
|
// "Next" should be disabled on the blank form
|
|
cy.findByRole("button", { name: "Next" }).should("be.disabled");
|
|
cy.findByLabelText("First name").type("Testy");
|
|
cy.findByLabelText("Last name").type("McTestface");
|
|
cy.findByLabelText("Email").type("testy@metabase.test");
|
|
cy.findByLabelText("Company or team name").type("Epic Team");
|
|
|
|
// test first with a weak password
|
|
cy.findByLabelText("Create a password").type("password");
|
|
cy.findByLabelText("Confirm your password").type("password");
|
|
|
|
// the form shouldn't be valid yet and we should display an error
|
|
cy.findByText("must include one number", { exact: false });
|
|
cy.findByRole("button", { name: "Next" }).should("be.disabled");
|
|
|
|
// now try a strong password that doesn't match
|
|
const strongPassword = "QJbHYJN3tPW[";
|
|
cy.findByLabelText(/^Create a password/)
|
|
.clear()
|
|
.type(strongPassword);
|
|
cy.findByLabelText(/^Confirm your password/)
|
|
.clear()
|
|
.type(strongPassword + "foobar")
|
|
.blur();
|
|
|
|
// tell the user about the mismatch after clicking "Next"
|
|
cy.findByRole("button", { name: "Next" }).should("be.disabled");
|
|
cy.findByText("passwords do not match", { exact: false });
|
|
|
|
// fix that mismatch
|
|
cy.findByLabelText(/^Confirm your password/)
|
|
.clear()
|
|
.type(strongPassword);
|
|
|
|
// Submit the first section
|
|
cy.findByText("Next").click();
|
|
|
|
// ========
|
|
// Usage question
|
|
// ========
|
|
|
|
cy.button("Next").click();
|
|
|
|
// ========
|
|
// Database
|
|
// ========
|
|
|
|
// The database step should be open
|
|
cy.findByText("Add your data");
|
|
|
|
// test database setup help card is NOT displayed before DB is selected
|
|
cy.findByText("Need help connecting?").should("not.be.visible");
|
|
|
|
// check database setup card is visible
|
|
cy.findByText("MySQL").click();
|
|
cy.findByText("Need help connecting?").should("be.visible");
|
|
cy.findByLabelText("Remove database").click();
|
|
cy.findByPlaceholderText("Search databases").type("SQL");
|
|
cy.findByText("SQLite").click();
|
|
cy.findByText("Need help connecting?");
|
|
|
|
// remove sqlite database
|
|
cy.findByLabelText("Remove database").click();
|
|
cy.findByText("Continue with sample data").click();
|
|
|
|
// test database setup help card is hidden on the next step
|
|
cy.findByText("Need help connecting?").should("not.be.visible");
|
|
|
|
skipLicenseStepOnEE();
|
|
|
|
// ================
|
|
// Data Preferences
|
|
// ================
|
|
|
|
// collection defaults to on and describes data collection
|
|
cy.findByText("All collection is completely anonymous.");
|
|
// turn collection off, which hides data collection description
|
|
cy.findByLabelText(
|
|
"Allow Metabase to anonymously collect usage events",
|
|
).click({ force: true });
|
|
|
|
cy.findByText("All collection is completely anonymous.").should(
|
|
"not.exist",
|
|
);
|
|
|
|
cy.findByText("Finish").click();
|
|
|
|
// ==================
|
|
// Finish & Subscribe
|
|
// ==================
|
|
cy.findByText("You're all set up!");
|
|
|
|
cy.findByText(
|
|
"Get infrequent emails about new releases and feature updates.",
|
|
);
|
|
|
|
cy.findByText("Take me to Metabase").click();
|
|
});
|
|
|
|
cy.location("pathname").should("eq", "/");
|
|
},
|
|
);
|
|
});
|
|
|
|
it("should set up Metabase without first name and last name (metabase#22754)", () => {
|
|
// This is a simplified version of the "scenarios > setup" test
|
|
cy.visit("/");
|
|
|
|
cy.location("pathname").should("eq", "/setup");
|
|
|
|
skipWelcomePage();
|
|
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
// User
|
|
fillUserAndContinue({
|
|
...admin,
|
|
company_name: "Epic team",
|
|
first_name: null,
|
|
last_name: null,
|
|
});
|
|
|
|
cy.findByText("Hi. Nice to meet you!");
|
|
|
|
cy.button("Next").click();
|
|
|
|
// Database
|
|
cy.findByText("Add your data");
|
|
cy.findByText("Continue with sample data").click();
|
|
|
|
skipLicenseStepOnEE();
|
|
|
|
// Turns off anonymous data collection
|
|
cy.findByLabelText(
|
|
"Allow Metabase to anonymously collect usage events",
|
|
).click({ force: true });
|
|
|
|
cy.findByText("All collection is completely anonymous.").should(
|
|
"not.exist",
|
|
);
|
|
cy.button("Finish").click();
|
|
|
|
// we need a mocked response (second parameter) to make sure we're not hitting the real endpoint
|
|
cy.intercept(SUBSCRIBE_URL, {}).as("subscribe");
|
|
|
|
// Finish & Subscribe
|
|
cy.findByLabelText(
|
|
"Get infrequent emails about new releases and feature updates.",
|
|
).click();
|
|
|
|
cy.findByText("Take me to Metabase").click();
|
|
});
|
|
|
|
cy.location("pathname").should("eq", "/");
|
|
|
|
H.main().findByText("Embed Metabase in your app").should("not.exist");
|
|
|
|
cy.wait("@subscribe").then(({ request }) => {
|
|
const formData = request.body;
|
|
// the body is encoded as formData, but it should contain the email in plan text
|
|
expect(formData).to.include(admin.email);
|
|
});
|
|
});
|
|
|
|
it("should pre-fill user info for hosted instances (infra-frontend#1109)", () => {
|
|
H.mockSessionProperty("is-hosted?", true);
|
|
|
|
cy.visit(
|
|
"/setup?first_name=John&last_name=Doe&email=john@doe.test&site_name=Doe%20Unlimited",
|
|
);
|
|
|
|
skipWelcomePage();
|
|
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
cy.findByDisplayValue("John").should("exist");
|
|
cy.findByDisplayValue("Doe").should("exist");
|
|
cy.findByDisplayValue("john@doe.test").should("exist");
|
|
cy.findByDisplayValue("Doe Unlimited").should("exist");
|
|
cy.findByLabelText("Create a password").should("be.empty");
|
|
});
|
|
});
|
|
|
|
it("should not show 'Sample Database' if env var is explicitly set to false during setup", () => {
|
|
H.mockSessionProperty("has-sample-database?", false);
|
|
|
|
navigateToDatabaseStep();
|
|
|
|
cy.findByLabelText("Add your data").within(() => {
|
|
cy.button("Continue with sample data").should("not.exist");
|
|
cy.button("I'll add my data later").click();
|
|
});
|
|
|
|
cy.log("We're done with the database step");
|
|
cy.findByLabelText("I'll add my own data later").should("be.visible");
|
|
});
|
|
|
|
it("should create a new user upon inviting a teammate", () => {
|
|
H.mockSessionProperty("email-configured?", true);
|
|
|
|
navigateToDatabaseStep();
|
|
cy.findByTestId("step-number").should("have.text", "3");
|
|
|
|
cy.findByLabelText("Setup section").click();
|
|
cy.findByLabelText("First name").type("TeammateFirstName");
|
|
cy.findByLabelText("Last name").type("TeammateLastName");
|
|
cy.findByLabelText("Email").type("teammate@metabase.test");
|
|
cy.intercept("POST", "/api/user").as("createUser");
|
|
|
|
cy.button("Send invitation").click();
|
|
|
|
cy.wait("@createUser").then((interception) => {
|
|
expect(interception.request.body).to.include({
|
|
first_name: "TeammateFirstName",
|
|
last_name: "TeammateLastName",
|
|
email: "teammate@metabase.test",
|
|
});
|
|
});
|
|
|
|
// Checks invite event was sent
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "invite_sent",
|
|
source: "setup",
|
|
});
|
|
|
|
// Checks we are now in the next step
|
|
cy.findByTestId("step-number").should("have.text", "4");
|
|
});
|
|
|
|
// There are only one step in the setup flow, so there is no need to show step numbers.
|
|
it("should not show step numbers in cloud embedding use case", () => {
|
|
H.mockSessionProperty("is-hosted?", true);
|
|
H.mockSessionProperty("token-features", { hosting: true });
|
|
|
|
cy.visit(
|
|
"/setup?first_name=John&last_name=Doe&email=john@doe.test&site_name=Doe%20Unlimited&use_case=embedding",
|
|
);
|
|
|
|
H.main().findByText("What should we call you?").should("be.visible");
|
|
cy.findByTestId("step-number").should("not.exist");
|
|
});
|
|
|
|
it("should update the site locale setting when changing language in setup", () => {
|
|
cy.intercept("PUT", "/api/setting/site-locale").as("updateSiteLocale");
|
|
|
|
cy.visit(
|
|
"/setup?first_name=John&last_name=Doe&email=john@doe.test&site_name=Doe%20Unlimited&use_case=embedding",
|
|
);
|
|
|
|
cy.findByTestId("step-number").should("have.text", "1");
|
|
|
|
cy.log("Switching language before user creation should not update setting");
|
|
selectLanguage("Dutch");
|
|
// A real translation, so a catalogue that never loads fails the test. The
|
|
// `[zz]` assertions below cannot cover this: they run after a second switch
|
|
// and would still pass if this first one had loaded nothing.
|
|
cy.findByTestId("setup-forms").button("Volgende").should("exist");
|
|
cy.get("@updateSiteLocale.all").should("have.length", 0);
|
|
selectLanguage("English (ZZ)");
|
|
cy.get("@updateSiteLocale.all").should("have.length", 0);
|
|
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
const password = "12341234";
|
|
|
|
cy.findByDisplayValue("John").should("exist");
|
|
cy.findByLabelText("[zz] Create a password").type(password);
|
|
cy.findByLabelText("[zz] Confirm your password").type(password);
|
|
cy.button("[zz] Next").click();
|
|
|
|
cy.findByLabelText("[zz] Hi, John. Nice to meet you!")
|
|
.scrollIntoView()
|
|
.should("be.visible");
|
|
});
|
|
|
|
cy.log(
|
|
"After user creation, switching the language should update the setting",
|
|
);
|
|
selectLanguage("English");
|
|
cy.get("@updateSiteLocale.all").should("have.length", 1);
|
|
selectLanguage("English (ZZ)");
|
|
cy.get("@updateSiteLocale.all").should("have.length", 2);
|
|
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
if (IS_ENTERPRISE) {
|
|
cy.findByText("[zz] Activate your commercial license").should(
|
|
"be.visible",
|
|
);
|
|
cy.findByText("[zz] I'll activate later").click();
|
|
}
|
|
cy.findByText("[zz] Finish").click();
|
|
cy.findByText("[zz] You're all set up!").should("be.visible");
|
|
cy.findByText("[zz] Take me to Metabase").click();
|
|
});
|
|
|
|
cy.location("pathname").should("eq", "/");
|
|
|
|
cy.log("Verify the final language English (ZZ) is preserved");
|
|
H.main()
|
|
.findByText("[zz] Get started with Embedding Metabase in your app")
|
|
.should("be.visible");
|
|
});
|
|
|
|
it("should allow you to connect a db during setup", () => {
|
|
const dbName = "SQLite db";
|
|
|
|
cy.intercept("GET", "api/collection/root").as("getRootCollection");
|
|
cy.intercept("GET", "api/database").as("getDatabases");
|
|
|
|
navigateToDatabaseStep();
|
|
|
|
cy.findByTestId("database-form").within(() => {
|
|
cy.findByPlaceholderText("Search databases").type("lite").blur();
|
|
cy.findByText("SQLite").click();
|
|
cy.findByLabelText("Display name").type(dbName);
|
|
cy.findByLabelText("Filename").type("./resources/sqlite-fixture.db", {
|
|
delay: 0,
|
|
});
|
|
cy.button("Connect database").click();
|
|
});
|
|
|
|
H.undoToastList().should("contain", `Connected to ${dbName}`);
|
|
|
|
skipLicenseStepOnEE();
|
|
|
|
// usage data
|
|
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
|
cy.get("section")
|
|
.last()
|
|
.findByText(/certain data about product usage/);
|
|
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
|
cy.get("section").last().button("Finish").click();
|
|
|
|
// done
|
|
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
|
cy.get("section")
|
|
.last()
|
|
.findByText(/You're all set up/);
|
|
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
|
cy.get("section")
|
|
.last()
|
|
.findByRole("link", { name: "Take me to Metabase" })
|
|
.click();
|
|
|
|
// in app
|
|
cy.location("pathname").should("eq", "/");
|
|
cy.wait(["@getRootCollection", "@getDatabases"]);
|
|
|
|
cy.get("main").within(() => {
|
|
cy.findByText("Here are some explorations of");
|
|
cy.findAllByRole("link").should("contain", dbName);
|
|
});
|
|
|
|
cy.visit("/browse/databases");
|
|
cy.findByTestId("database-browser").findByText(dbName);
|
|
});
|
|
|
|
it("embedded use-case, it should hide the db step and show the embedding homepage", () => {
|
|
cy.intercept("GET", "/api/activity/recents*").as("getRecents");
|
|
cy.intercept("GET", "/api/collection/root").as("getRootCollection");
|
|
cy.intercept("GET", "/api/database").as("getDatabases");
|
|
|
|
cy.visit("/setup");
|
|
|
|
cy.location("pathname").should("eq", "/setup");
|
|
|
|
skipWelcomePage();
|
|
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
// User
|
|
fillUserAndContinue({
|
|
...admin,
|
|
company_name: "Epic team",
|
|
first_name: null,
|
|
last_name: null,
|
|
});
|
|
|
|
cy.findByText("Hi. Nice to meet you!");
|
|
|
|
cy.findByText("Embedding analytics into my application").click();
|
|
cy.button("Next").click();
|
|
|
|
// Database
|
|
cy.findByText("Add your data").should("not.exist");
|
|
|
|
skipLicenseStepOnEE();
|
|
|
|
// Turns off anonymous data collection
|
|
cy.findByLabelText(
|
|
"Allow Metabase to anonymously collect usage events",
|
|
).click({ force: true });
|
|
|
|
cy.findByText("All collection is completely anonymous.").should(
|
|
"not.exist",
|
|
);
|
|
|
|
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
|
cy.get("section")
|
|
.last()
|
|
.findByText(/certain data about product usage/);
|
|
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
|
cy.get("section").last().button("Finish").click();
|
|
|
|
// Finish & Subscribe
|
|
cy.intercept("GET", "/api/session/properties").as("properties");
|
|
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
|
cy.get("section")
|
|
.last()
|
|
.findByRole("link", { name: "Take me to Metabase" })
|
|
.click();
|
|
});
|
|
|
|
cy.location("pathname").should("eq", "/");
|
|
|
|
H.main()
|
|
.findByText("Get started with Embedding Metabase in your app")
|
|
.should("exist");
|
|
|
|
// should persist page loads
|
|
cy.reload();
|
|
cy.wait([
|
|
"@getRecents",
|
|
"@getRootCollection",
|
|
"@getDatabases",
|
|
"@properties",
|
|
]);
|
|
|
|
H.main()
|
|
.findByText("Get started with Embedding Metabase in your app")
|
|
.should("be.visible");
|
|
|
|
H.main().scrollTo("top");
|
|
|
|
H.main().findByText("Hide these").trigger("mouseover");
|
|
|
|
H.popover().findByText("Embedding done, all good").click();
|
|
|
|
H.main()
|
|
.findByText("Get started with Embedding Metabase in your app")
|
|
.should("not.exist");
|
|
});
|
|
});
|
|
|
|
describe("scenarios > setup > AI config step", () => {
|
|
const MOCK_LLM_PORT = 6123;
|
|
|
|
beforeEach(() => {
|
|
H.restore("blank");
|
|
});
|
|
|
|
afterEach(() => {
|
|
cy.task("stopMockLlmServer");
|
|
});
|
|
|
|
const navigateToAiConfigStep = () => {
|
|
navigateToDatabaseStep();
|
|
cy.findByTestId("setup-forms")
|
|
.findByText("Continue with sample data")
|
|
.click();
|
|
finishSetup();
|
|
startAiConfigStep();
|
|
};
|
|
|
|
it("should offer BYOK providers without the managed option and allow skipping", () => {
|
|
navigateToAiConfigStep();
|
|
|
|
cy.findByLabelText("Connect to an AI provider").within(() => {
|
|
[/OpenAI/, /OpenRouter/, /Microsoft Azure/, /Amazon Bedrock/].forEach(
|
|
(name) => {
|
|
cy.findByRole("button", { name })
|
|
.scrollIntoView()
|
|
.should("be.visible");
|
|
},
|
|
);
|
|
// the managed provider is offered but not connectable without the LLM proxy,
|
|
// which e2e does not configure
|
|
cy.findByRole("button", { name: /Metabase/ }).should("be.disabled");
|
|
|
|
cy.findByRole("button", { name: /Anthropic/ }).click();
|
|
cy.findByLabelText(/API key/).should("be.visible");
|
|
cy.button("I'll set this up later").click();
|
|
});
|
|
|
|
cy.findByLabelText("I'll set up AI later").should("be.visible");
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
cy.findByText("You're all set up!").should("be.visible");
|
|
cy.button("Set up AI").should("not.exist");
|
|
});
|
|
});
|
|
|
|
it("should connect an Anthropic API key during setup", () => {
|
|
cy.task("startMockLlmServer", {
|
|
port: MOCK_LLM_PORT,
|
|
responseText: "Hello from mock LLM!",
|
|
});
|
|
cy.intercept("POST", "/api/llm/providers").as("connectProvider");
|
|
|
|
navigateToAiConfigStep();
|
|
|
|
cy.findByLabelText("Connect to an AI provider").within(() => {
|
|
cy.findByRole("button", { name: /Anthropic/ }).click();
|
|
cy.findByLabelText(/API key/).type("sk-ant-api03-e2e-test-key");
|
|
|
|
// The base URL belongs to the connection now rather than to a global setting, so
|
|
// this is what points the credential-verifying /v1/models call at the mock server.
|
|
cy.findByRole("button", { name: /Advanced settings/ }).click();
|
|
cy.findByLabelText("API base URL").type(
|
|
`http://localhost:${MOCK_LLM_PORT}`,
|
|
);
|
|
|
|
cy.button("Connect").click();
|
|
cy.wait("@connectProvider");
|
|
|
|
cy.findByLabelText("Model").should("be.visible");
|
|
cy.button("Done").click();
|
|
});
|
|
|
|
cy.findByLabelText("Connected to Anthropic").should("be.visible");
|
|
});
|
|
|
|
it("should connect the Metabase managed provider during setup", () => {
|
|
// The "Metabase" provider option is gated on token features read at page
|
|
// bootstrap (plugin init), before any interceptable API call — so patch
|
|
// the server-embedded payload as it is assigned. We can't use
|
|
// `H.activateToken` here because the endpoint requires a user.. which
|
|
// isn't created yet.
|
|
// The store-backed calls (add-ons pricing, the managed connect itself)
|
|
// have no real backing in e2e and are stubbed at the network layer.
|
|
type BootstrapPayload = {
|
|
"token-features"?: Record<string, boolean>;
|
|
} & Record<string, unknown>;
|
|
|
|
const enableManagedAiFeatures = (win: Cypress.AUTWindow) => {
|
|
let bootstrap: BootstrapPayload | undefined;
|
|
Object.defineProperty(win, "MetabaseBootstrap", {
|
|
configurable: true,
|
|
get: () => bootstrap,
|
|
set: (value: BootstrapPayload) => {
|
|
bootstrap = {
|
|
...value,
|
|
"token-features": {
|
|
...value["token-features"],
|
|
"metabase-ai-managed": true,
|
|
"offer-metabase-ai-managed": true,
|
|
},
|
|
};
|
|
},
|
|
});
|
|
};
|
|
|
|
const managedConnection = {
|
|
key: "metabase",
|
|
type: "metabase",
|
|
name: "Metabase AI service",
|
|
source: "db",
|
|
usable: true,
|
|
env_vars: [],
|
|
config: {},
|
|
};
|
|
|
|
let isConnected = false;
|
|
cy.intercept("GET", "/api/session/properties", (req) => {
|
|
req.reply((res) => {
|
|
res.body["token-features"] = {
|
|
...res.body["token-features"],
|
|
"metabase-ai-managed": true,
|
|
"offer-metabase-ai-managed": true,
|
|
};
|
|
if (isConnected) {
|
|
res.body["llm-metabot-configured?"] = true;
|
|
res.body["llm-metabot-provider"] =
|
|
"metabase/anthropic/claude-sonnet-4-6";
|
|
}
|
|
});
|
|
});
|
|
// the managed type is only connectable once the LLM proxy is configured, which has no
|
|
// backing in e2e, so mark it available the same way the token features are patched
|
|
cy.intercept("GET", "/api/llm/provider-types", (req) => {
|
|
req.reply((res) => {
|
|
res.body = res.body.map((providerType: { type: string }) =>
|
|
providerType.type === "metabase"
|
|
? { ...providerType, available: true }
|
|
: providerType,
|
|
);
|
|
});
|
|
});
|
|
cy.intercept("GET", "/api/llm/providers", (req) => {
|
|
req.reply(isConnected ? [managedConnection] : []);
|
|
});
|
|
cy.intercept("GET", "/api/ee/cloud-add-ons/addons", [
|
|
{
|
|
id: 1,
|
|
active: true,
|
|
self_service: true,
|
|
deployment: "hosting",
|
|
billing_period_months: 1,
|
|
default_base_fee: 0,
|
|
default_included_units: 0,
|
|
default_prepaid_units: 1,
|
|
free_units: 1_000_000,
|
|
default_price_per_unit: 3.75 / 1_000_000,
|
|
default_total_units: 1,
|
|
description: null,
|
|
is_metered: true,
|
|
name: "Metabase AI Managed",
|
|
product_tiers: [],
|
|
product_type: "metabase-ai-managed",
|
|
short_name: "Metabase AI Managed",
|
|
token_features: [],
|
|
trial_days: null,
|
|
},
|
|
]);
|
|
cy.intercept("GET", "/api/ee/metabot/usage", {
|
|
is_locked: false,
|
|
tokens: 0,
|
|
free_tokens: 1000000,
|
|
updated_at: null,
|
|
});
|
|
cy.intercept("POST", "/api/premium-features/token/refresh", {});
|
|
cy.intercept("POST", "/api/llm/providers", (req) => {
|
|
isConnected = true;
|
|
req.reply(managedConnection);
|
|
}).as("connectManaged");
|
|
|
|
cy.visit(
|
|
"/setup?first_name=John&last_name=Doe&email=john@doe.test&site_name=Doe%20Unlimited",
|
|
{ onBeforeLoad: enableManagedAiFeatures },
|
|
);
|
|
|
|
skipWelcomePage();
|
|
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
const password = "12341234";
|
|
cy.findByLabelText("Create a password").type(password);
|
|
cy.findByLabelText("Confirm your password").type(password);
|
|
cy.button("Next").click();
|
|
|
|
cy.findByLabelText("What will you use Metabase for?").should(
|
|
"be.visible",
|
|
);
|
|
cy.button("Next").click();
|
|
|
|
cy.findByText("Continue with sample data").click();
|
|
|
|
// the patched token features make this a paid plan, so the license step
|
|
// is skipped even on EE
|
|
cy.findByText("Finish").click();
|
|
});
|
|
|
|
startAiConfigStep();
|
|
|
|
cy.findByLabelText("Connect to an AI provider").within(() => {
|
|
cy.findByRole("button", { name: /Metabase/ }).click();
|
|
cy.findByText(
|
|
/The simplest way to get started with AI in Metabase/,
|
|
).should("be.visible");
|
|
cy.findByText(/You get 1M tokens for free/).should("be.visible");
|
|
cy.button("Connect").click();
|
|
});
|
|
|
|
cy.wait("@connectManaged")
|
|
.its("request.body")
|
|
.should("deep.equal", { type: "metabase" });
|
|
cy.findByLabelText("Connected to Metabase AI service").should("be.visible");
|
|
});
|
|
|
|
it("should not offer the step when AI features are disabled", () => {
|
|
H.mockSessionProperty("ai-features-enabled?", false);
|
|
|
|
navigateToDatabaseStep();
|
|
cy.findByTestId("setup-forms")
|
|
.findByText("Continue with sample data")
|
|
.click();
|
|
|
|
skipLicenseStepOnEE();
|
|
cy.findByLabelText("Usage data preferences").should("be.visible");
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
cy.findByText("Finish").click();
|
|
|
|
cy.findByText("You're all set up!").should("be.visible");
|
|
cy.button("Set up AI").should("not.exist");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("scenarios > setup (EE)", () => {
|
|
beforeEach(() => H.restore("blank"));
|
|
|
|
it("should ask for a license token on self-hosted", () => {
|
|
cy.visit("/setup");
|
|
|
|
skipWelcomePage();
|
|
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
fillUserAndContinue({
|
|
...admin,
|
|
company_name: "Epic team",
|
|
});
|
|
|
|
cy.button("Next").click();
|
|
|
|
cy.findByText("Continue with sample data").click();
|
|
|
|
cy.findByText("Activate your commercial license").should("exist");
|
|
|
|
// Use cy.env() for sensitive token values (async API)
|
|
cy.env(["MB_STARTER_CLOUD_TOKEN"]).then(({ MB_STARTER_CLOUD_TOKEN }) => {
|
|
typeToken(MB_STARTER_CLOUD_TOKEN);
|
|
});
|
|
|
|
cy.button("Activate").click();
|
|
|
|
cy.findByText("Finish").click();
|
|
cy.findByText("Take me to Metabase").click();
|
|
});
|
|
|
|
cy.intercept("/api/premium-features/token/status").as("tokenStatus");
|
|
|
|
cy.visit("/admin/settings/license");
|
|
|
|
H.main().findByText("Looking for more?").should("exist");
|
|
|
|
cy.wait("@tokenStatus").then((request) => {
|
|
expect(request.response?.body.valid).to.equal(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("scenarios > setup", () => {
|
|
beforeEach(() => {
|
|
H.restore("blank");
|
|
H.resetSnowplow();
|
|
});
|
|
|
|
afterEach(() => {
|
|
H.expectNoBadSnowplowEvents();
|
|
});
|
|
|
|
it("should send snowplow events", () => {
|
|
cy.visit("/setup");
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "step_seen",
|
|
step_number: 0,
|
|
step: "welcome",
|
|
});
|
|
skipWelcomePage();
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "step_seen",
|
|
step_number: 1,
|
|
step: "user_info",
|
|
});
|
|
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
fillUserAndContinue({
|
|
...admin,
|
|
company_name: "Epic team",
|
|
});
|
|
|
|
cy.findByText("What will you use Metabase for?").should("exist");
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "step_seen",
|
|
step_number: 2,
|
|
step: "usage_question",
|
|
});
|
|
cy.button("Next").click();
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "usage_reason_selected",
|
|
usage_reason: "self-service-analytics",
|
|
});
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "step_seen",
|
|
step_number: 3,
|
|
step: "db_connection",
|
|
});
|
|
cy.findByText("Continue with sample data").click();
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "add_data_later_clicked",
|
|
});
|
|
|
|
// This step is only visile on EE builds
|
|
if (IS_ENTERPRISE) {
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "step_seen",
|
|
step_number: 4,
|
|
step: "license_token",
|
|
});
|
|
|
|
cy.button("I'll activate later").click();
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "license_token_step_submitted",
|
|
valid_token_present: false,
|
|
});
|
|
}
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "step_seen",
|
|
step_number: IS_ENTERPRISE ? 5 : 4,
|
|
step: "data_usage",
|
|
});
|
|
|
|
cy.findByRole("button", { name: "Finish" }).click();
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "step_seen",
|
|
step_number: IS_ENTERPRISE ? 6 : 5,
|
|
step: "completed",
|
|
});
|
|
|
|
cy.button("Set up AI").click();
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "ai_setup_started",
|
|
triggered_from: "setup",
|
|
});
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "step_seen",
|
|
step_number: IS_ENTERPRISE ? 6 : 5,
|
|
step: "ai_config",
|
|
});
|
|
|
|
cy.button("I'll set this up later").click();
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "ai_setup_later_clicked",
|
|
triggered_from: "setup",
|
|
});
|
|
|
|
cy.findByLabelText(
|
|
"Get infrequent emails about new releases and feature updates.",
|
|
).click();
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "newsletter-toggle-clicked",
|
|
triggered_from: "setup",
|
|
event_detail: "opted-in",
|
|
});
|
|
|
|
cy.findByLabelText(
|
|
"Get infrequent emails about new releases and feature updates.",
|
|
).click();
|
|
|
|
H.expectUnstructuredSnowplowEvent({
|
|
event: "newsletter-toggle-clicked",
|
|
triggered_from: "setup",
|
|
event_detail: "opted-out",
|
|
});
|
|
});
|
|
});
|
|
|
|
it("should ignore snowplow failures and work as normal", () => {
|
|
H.blockSnowplow();
|
|
cy.visit("/setup");
|
|
skipWelcomePage();
|
|
cy.findByLabelText("First name").should("be.visible");
|
|
cy.findByLabelText("Email").should("be.visible");
|
|
H.assertNoUnstructuredSnowplowEvent({
|
|
event: "step_seen",
|
|
});
|
|
});
|
|
});
|
|
|
|
const skipWelcomePage = () => {
|
|
cy.findByTestId("welcome-page").within(() => {
|
|
cy.findByText("Welcome to Metabase");
|
|
cy.findByText("Let's get started").click();
|
|
});
|
|
};
|
|
|
|
const fillUserAndContinue = ({
|
|
email,
|
|
first_name,
|
|
last_name,
|
|
password,
|
|
company_name,
|
|
}: {
|
|
first_name?: string | null;
|
|
last_name?: string | null;
|
|
email?: string | null;
|
|
password?: string | null;
|
|
company_name?: string | null;
|
|
}) => {
|
|
cy.findByText("What should we call you?");
|
|
|
|
if (first_name) {
|
|
cy.findByLabelText("First name").type(first_name);
|
|
}
|
|
if (last_name) {
|
|
cy.findByLabelText("Last name").type(last_name);
|
|
}
|
|
if (email) {
|
|
cy.findByLabelText("Email").type(email);
|
|
}
|
|
if (company_name) {
|
|
cy.findByLabelText("Company or team name").type(company_name);
|
|
}
|
|
if (password) {
|
|
cy.findByLabelText("Create a password").type(password);
|
|
}
|
|
if (password) {
|
|
cy.findByLabelText("Confirm your password").type(password);
|
|
}
|
|
cy.button("Next").click();
|
|
};
|
|
|
|
const skipLicenseStepOnEE = () => {
|
|
if (IS_ENTERPRISE) {
|
|
cy.findByText("Activate your commercial license").should("exist");
|
|
cy.button("I'll activate later").click();
|
|
}
|
|
};
|
|
|
|
const finishSetup = () => {
|
|
skipLicenseStepOnEE();
|
|
cy.findByText("Finish").click();
|
|
};
|
|
|
|
const startAiConfigStep = () => {
|
|
cy.findByText("You're all set up!").should("be.visible");
|
|
cy.button("Set up AI").click();
|
|
cy.findByLabelText("Connect to an AI provider").should("be.visible");
|
|
};
|
|
|
|
const typeToken = (token: string) => {
|
|
// hides the requests from the logs as the token is passed as a GET param
|
|
cy.intercept({ resourceType: "xhr" }, { log: false });
|
|
cy.findByLabelText("Token")
|
|
// hides the token from failure screenshots
|
|
.invoke("attr", "type", "password")
|
|
.type(token, { log: false });
|
|
};
|
|
|
|
// Navigate to the setup page, fills user data, password, skips usage questionnaire and proceeds to the database step
|
|
const navigateToDatabaseStep = () => {
|
|
cy.visit(
|
|
"/setup?first_name=John&last_name=Doe&email=john@doe.test&site_name=Doe%20Unlimited",
|
|
);
|
|
|
|
skipWelcomePage();
|
|
|
|
cy.findByTestId("setup-forms").within(() => {
|
|
const password = "12341234";
|
|
cy.findByLabelText("Create a password").should("be.empty").type(password);
|
|
cy.findByLabelText("Confirm your password").type(password);
|
|
cy.button("Next").click();
|
|
|
|
cy.log("Just go through the usage questionnaire");
|
|
cy.findByLabelText("What will you use Metabase for?").should("be.visible");
|
|
cy.button("Next").click();
|
|
});
|
|
|
|
cy.log("We are now on the database step");
|
|
};
|
|
|
|
const selectLanguage = (targetLanguage: string) => {
|
|
cy.findByTestId("language-selector").click();
|
|
|
|
cy.log("Select language from dropdown");
|
|
H.popover()
|
|
.findByText(targetLanguage)
|
|
.scrollIntoView()
|
|
.should("be.visible")
|
|
.click();
|
|
|
|
// Wait on the selection landing rather than on the catalogue request. The
|
|
// catalogue is a hashed chunk now, so a repeat of a language already loaded
|
|
// in this browser is served from cache and makes no request at all. Every
|
|
// caller that needs the catalogue to have been applied asserts a `[zz]`
|
|
// string right after.
|
|
cy.findByTestId("language-selector").should("have.value", targetLanguage);
|
|
};
|