Metabase e2e-test-create

install
source · Clone the upstream repo
git clone https://github.com/metabase/metabase
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/metabase/metabase "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/e2e-test-create" ~/.claude/skills/metabase-metabase-e2e-test-create && rm -rf "$T"
manifest: .claude/skills/e2e-test-create/SKILL.md
source content

Code-Reading-First → Generate Cypress Tests (Metabase)

You are writing Cypress E2E tests for the Metabase codebase. Before generating ANY test code, you MUST analyze React component source code to understand DOM structure, selectors, and user flows.

Phase 0 — Research

  1. Read existing helpers before writing anything:
    • e2e/support/helpers/
      — all shared helpers (restore, signInAs, openOrdersTable, etc.)
    • e2e/support/cypress_sample_database.ts
      — table/field schema constants (ORDERS, PRODUCTS, etc.)
    • e2e/support/cypress_sample_instance_data.ts
      — instance-specific IDs (ORDERS_DASHBOARD_ID, NORMAL_USER_ID, etc.)
  2. Glob
    e2e/test/scenarios/
    to find the closest existing spec to the area under test. Study its patterns — match them exactly.
  3. Glob
    frontend/src/metabase/
    to find React components for the feature area.

Phase 1 — Code Analysis

Read React component source to understand DOM structure. No browser needed — source code has everything.

  1. Find relevant components: Glob and grep
    frontend/src/metabase/
    for the feature area.
  2. Extract selectors: Grep for
    data-testid
    in relevant components.
  3. Note visible text: Read component JSX for button labels, headings, placeholders.
  4. Note aria attributes: Grep for
    aria-label
    in relevant components.
  5. Understand user flows: Read event handlers (onClick, onSubmit, onChange) to understand interactions.
  6. Find API calls: Grep for
    Api.use
    ,
    fetch
    ,
    useQuery
    , endpoint definitions to identify API calls to intercept.
  7. Cross-reference with existing specs: Find specs in the same area and reuse their proven selectors and
    cy.intercept
    patterns.

Phase 2 — Start Backend

Use

MB_EDITION=oss
by default. Only use
MB_EDITION=ee
when the user explicitly asks to write an enterprise test.

Start the backend using

run_in_background: true
(NOT
&
).
bin/e2e-backend
automatically detects if a backend is already running and reuses it.

MB_EDITION=oss bin/e2e-backend

Do NOT manually generate snapshots by running unrelated test specs. The

bun test-cypress
runner has
GENERATE_SNAPSHOTS: true
by default and automatically generates snapshots before running any spec. When running tests in Phase 4 via the
/e2e-test
skill, snapshots will be generated on the first run if they don't already exist.

Restore clean test data:

curl -sf -X POST http://localhost:4000/api/testing/restore/default

Phase 3 — Generate Cypress Spec

File location & naming

Place specs in

e2e/test/scenarios/<area>/
mirroring the URL structure. Name:
<feature>.cy.spec.js
(NOT
.cy.spec.ts
).

Metabase conventions (mandatory)

  • Helpers via
    cy.H
    : All helpers are accessed via
    const { H } = cy;
    — NOT via direct imports from
    e2e/support/helpers
    .
const { H } = cy;

describe("feature name", () => {
  beforeEach(() => {
    H.restore();
    cy.signInAsAdmin();
  });
});
  • Sample Database schema (table/field definitions): Import from
    cypress_sample_database
    .
import { ORDERS, ORDERS_ID, PRODUCTS } from "e2e/support/cypress_sample_database";
  • Instance data (dashboard IDs, user IDs, etc.): Import from
    cypress_sample_instance_data
    .
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
  • Selectors (priority order):

    1. cy.findByText()
      /
      cy.findByLabelText()
      /
      cy.findByRole()
      — from
      @testing-library/cypress
    2. cy.findByTestId()
      — for
      data-testid
      attributes
    3. cy.get("[data-testid='...']")
      — fallback
    4. NEVER use positional selectors, CSS class names, or XPaths.
  • Navigation helpers: Use existing helpers like

    H.openOrdersTable()
    ,
    H.openNativeEditor()
    ,
    H.visitDashboard(id)
    ,
    H.visitQuestion(id)
    instead of raw
    cy.visit()
    chains. Grep
    e2e/support/helpers/
    to discover what's available for your area.

  • API setup over UI setup: Use

    cy.request()
    or existing API helpers to set up state. Only use the UI for the flow you're actually testing.

  • Assertions: Assert on visible text, URL, aria state — not DOM structure.

  • Waits: Never use

    cy.wait(ms)
    . Use
    cy.intercept()
    +
    cy.wait("@alias")
    for API calls, or
    cy.findByText().should("be.visible")
    for DOM readiness.

  • Isolation: Each

    it()
    block must be independently runnable. Don't depend on state from a previous
    it()
    .

Spec structure template

const { H } = cy;
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
import { ORDERS, ORDERS_ID } from "e2e/support/cypress_sample_database";

describe("area > sub-area > feature (#issue-number)", () => {
  beforeEach(() => {
    H.restore();
    cy.signInAsAdmin();
  });

  it("should do the primary happy-path thing", () => {
    // test
  });

  it("should handle the edge case", () => {
    // test
  });
});

Intercepts

When you identified API calls during code analysis, stub or wait on them:

cy.intercept("POST", "/api/dataset").as("dataset");
// ... trigger action ...
cy.wait("@dataset");

Phase 4 — Validate

After generating specs:

  1. Check that all imported helpers exist (Grep
    e2e/support/helpers/
    ).
  2. You MUST use the
    /e2e-test
    skill
    to run tests — do NOT run
    bun test-cypress
    directly. The
    /e2e-test
    skill handles edition selection, snapshot management, and correct env vars.
    /e2e-test GREP="should do the thing" --spec e2e/test/scenarios/<path>
    If you created multiple
    it()
    blocks, run each one individually to isolate failures.

Phase 5 — Fix Failures (up to 2 attempts)

When a test fails, try to fix it from Cypress output first:

  1. Read the failure screenshot (path printed under
    (Screenshots)
    ).
  2. Read the error message and code frame from the console output.
  3. Fix the test and re-run (back to Phase 4, step 2).

If you cannot diagnose the issue after 2 attempts, proceed to Phase 6.

Phase 6 — Playwright Fallback

Only reach this phase after 2 failed fix attempts from Phase 5. The backend is already running.

Restore clean test data:

curl -sf -X POST http://localhost:4000/api/testing/restore/default

Bypass CSP headers before navigating (Metabase serves strict CSP that blocks dev server scripts). Use

browser_run_code
to set this up:

async (page) => {
  // Strip CSP headers so the page loads (mirrors Cypress chromeWebSecurity: false)
  await page.context().route('**/*', async (route) => {
    const response = await route.fetch();
    const headers = { ...response.headers() };
    delete headers['content-security-policy'];
    delete headers['content-security-policy-report-only'];
    await route.fulfill({ response, headers });
  });

  // Sign in via API
  const response = await page.request.post('http://localhost:4000/api/session', {
    data: { username: 'admin@metabase.test', password: '12341234' }
  });
  const session = await response.json();
  await page.context().addCookies([{
    name: 'metabase.DEVICE',
    value: session.id,
    domain: 'localhost',
    path: '/'
  }]);

  await page.goto('http://localhost:4000');
  await page.waitForLoadState('networkidle');
  return 'signed in';
}

Maintain an observation log incrementally. After EVERY significant Playwright interaction, IMMEDIATELY append what you observed to the scratch file BEFORE performing the next interaction:

cat >> /tmp/e2e-observations.md << 'OBSERVATION'
## [Page/Flow name]
- URL: /question/notebook#...
- Clicked: "Box plot" button → visible text "Box plot", role: radio
- Selectors: data-testid="viz-type-button", findByText("Box plot")
- API call: POST /api/dataset (triggered on viz change)
- Key state: after selecting viz type, summary sidebar shows metric picker
OBSERVATION

For each page/flow:

  • Take an accessibility snapshot (
    browser_snapshot
    ).
  • Click through interactive elements, fill forms, trigger modals.
  • Append to the observation log immediately after each step: URLs, visible text, aria labels,
    data-testid
    attrs, API calls.
  • Screenshot key states.

After exploration:

  1. Read back your observation log:
    cat /tmp/e2e-observations.md
  2. Fix the test using observed selectors and behavior.
  3. Re-run the test (back to Phase 4).
  4. Clean up:
    rm -f /tmp/e2e-observations.md

Phase 7 — Cleanup

After all tests pass (or after giving up on fixing failures), always kill the backend on port 4000:

lsof -ti:4000 | xargs kill 2>/dev/null || true

Do NOT use broad

pkill
patterns — there may be other Metabase instances on different ports. The backend process started in Phase 2 will NOT be killed automatically when the Claude session ends. Leaving it running wastes resources and can interfere with future sessions. Always clean up.

What NOT to do

  • Do NOT use Playwright as the first step — always analyze source code first.
  • Do NOT kill the backend between phases — it stays running throughout.
  • Do NOT invent selectors you didn't find in source code or observe in the browser.
  • Do NOT hardcode database IDs — import from
    cypress_sample_database
    or
    cypress_sample_instance_data
    .
  • Do NOT use
    cy.wait(1000)
    or any numeric wait.
  • Do NOT create setup flows through the UI when an API helper exists.
  • Do NOT put tests in
    cypress/e2e/
    — Metabase uses
    e2e/test/scenarios/
    .
  • Do NOT import helpers directly from
    e2e/support/helpers
    — use
    const { H } = cy;
    .