You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2025/11/15 18:22:18 UTC

(superset) 07/18: feat(playwright): implement dataset delete E2E test with API infrastructure

This is an automated email from the ASF dual-hosted git repository.

jli pushed a commit to branch feat/dataset-playwright-tests
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 7a678c925e2728d98899428530ed9e41eee6bded
Author: Joe Li <jo...@preset.io>
AuthorDate: Wed Nov 5 16:57:26 2025 -0800

    feat(playwright): implement dataset delete E2E test with API infrastructure
    
    Add comprehensive API infrastructure and delete dataset E2E test:
    
    **API Infrastructure:**
    - Generic request helpers with CSRF token handling (requests.ts)
    - DRY header building for all mutation requests
    - Database and dataset CRUD operations
    - Google Sheets database factory for test data isolation
    - Response validation with clear error messages
    
    **Toast Component:**
    - Reusable toast verification component
    - Type-specific getters (success, danger, warning, info)
    - SELECTORS constants (no magic strings)
    
    **Delete Dataset Test:**
    - Creates test dataset via API (Google Sheets)
    - Tests full delete flow: click → modal → confirm → verify removal
    - Validates success toast appears with correct message
    - Clean afterEach cleanup pattern (no race conditions)
    
    **Code Quality:**
    - No private Playwright APIs (uses env variables)
    - Path prefix support for prefixed deployments
    - All selectors in constants
    - Type-safe throughout
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <no...@anthropic.com>
---
 .../playwright/components/core/Table.ts            |  10 +
 .../playwright/components/core/Toast.ts            | 105 ++++++++
 superset-frontend/playwright/helpers/api/client.ts | 154 ------------
 .../playwright/helpers/api/database.factories.ts   |  81 ++++++
 .../playwright/helpers/api/database.ts             |  52 ++++
 .../playwright/helpers/api/dataset.factories.ts    |  64 +++++
 .../playwright/helpers/api/dataset.ts              |  52 ++++
 .../playwright/helpers/api/datasets.ts             | 272 ---------------------
 .../playwright/helpers/api/requests.ts             | 178 ++++++++++++++
 .../playwright/pages/DatasetListPage.ts            |  36 +++
 .../playwright/tests/dataset/dataset-list.spec.ts  |  77 +++++-
 11 files changed, 654 insertions(+), 427 deletions(-)

diff --git a/superset-frontend/playwright/components/core/Table.ts b/superset-frontend/playwright/components/core/Table.ts
index aa8ae40e52..84dd138b90 100644
--- a/superset-frontend/playwright/components/core/Table.ts
+++ b/superset-frontend/playwright/components/core/Table.ts
@@ -69,4 +69,14 @@ export class Table {
   async waitForVisible(options?: { timeout?: number }): Promise<void> {
     await this.element.waitFor({ state: 'visible', ...options });
   }
+
+  /**
+   * Clicks an action button in a row by selector
+   * @param rowText - Text to identify the row
+   * @param selector - CSS selector for the action element
+   */
+  async clickRowAction(rowText: string, selector: string): Promise<void> {
+    const row = this.getRow(rowText);
+    await row.locator(selector).first().click();
+  }
 }
diff --git a/superset-frontend/playwright/components/core/Toast.ts b/superset-frontend/playwright/components/core/Toast.ts
new file mode 100644
index 0000000000..a291e03c0d
--- /dev/null
+++ b/superset-frontend/playwright/components/core/Toast.ts
@@ -0,0 +1,105 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Page, Locator } from '@playwright/test';
+
+export type ToastType = 'success' | 'danger' | 'warning' | 'info';
+
+const SELECTORS = {
+  CONTAINER: '[data-test="toast-container"][role="alert"]',
+  CONTENT: '.toast__content',
+  CLOSE_BUTTON: '[data-test="close-button"]',
+} as const;
+
+/**
+ * Toast notification component
+ * Handles success, danger, warning, and info toasts
+ */
+export class Toast {
+  private page: Page;
+  private container: Locator;
+
+  constructor(page: Page) {
+    this.page = page;
+    this.container = page.locator(SELECTORS.CONTAINER);
+  }
+
+  /**
+   * Get the toast container locator
+   */
+  get(): Locator {
+    return this.container;
+  }
+
+  /**
+   * Get the toast message text
+   */
+  getMessage(): Locator {
+    return this.container.locator(SELECTORS.CONTENT);
+  }
+
+  /**
+   * Wait for a toast to appear
+   */
+  async waitForVisible(): Promise<void> {
+    await this.container.waitFor({ state: 'visible' });
+  }
+
+  /**
+   * Wait for toast to disappear
+   */
+  async waitForHidden(): Promise<void> {
+    await this.container.waitFor({ state: 'hidden' });
+  }
+
+  /**
+   * Get a success toast
+   */
+  getSuccess(): Locator {
+    return this.page.locator(`${SELECTORS.CONTAINER}.toast--success`);
+  }
+
+  /**
+   * Get a danger/error toast
+   */
+  getDanger(): Locator {
+    return this.page.locator(`${SELECTORS.CONTAINER}.toast--danger`);
+  }
+
+  /**
+   * Get a warning toast
+   */
+  getWarning(): Locator {
+    return this.page.locator(`${SELECTORS.CONTAINER}.toast--warning`);
+  }
+
+  /**
+   * Get an info toast
+   */
+  getInfo(): Locator {
+    return this.page.locator(`${SELECTORS.CONTAINER}.toast--info`);
+  }
+
+  /**
+   * Close the toast by clicking the close button
+   */
+  async close(): Promise<void> {
+    await this.container.locator(SELECTORS.CLOSE_BUTTON).click();
+  }
+}
diff --git a/superset-frontend/playwright/helpers/api/client.ts b/superset-frontend/playwright/helpers/api/client.ts
deleted file mode 100644
index 0dcd5ee054..0000000000
--- a/superset-frontend/playwright/helpers/api/client.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { APIRequestContext, Page } from '@playwright/test';
-
-/**
- * Gets the CSRF token from localStorage via page evaluation.
- * Required for authenticated API requests to Superset backend.
- *
- * @param page - Playwright page object (must be authenticated)
- * @returns CSRF access token string
- */
-export async function getCSRFToken(page: Page): Promise<string> {
-  const token = await page.evaluate(() =>
-    window.localStorage.getItem('access_token'),
-  );
-
-  if (!token) {
-    throw new Error(
-      'CSRF token not found in localStorage. Ensure the page is authenticated.',
-    );
-  }
-
-  return token;
-}
-
-/**
- * Extracts the base URL from the current page URL.
- * Falls back to PLAYWRIGHT_BASE_URL env var, then localhost.
- *
- * @param page - Playwright page object
- * @returns Base URL (e.g., 'http://localhost:8088' or 'http://superset:8088' in CI)
- */
-function getBaseURL(page: Page): string {
-  const currentUrl = page.url();
-
-  // If page has navigated, extract base URL from current URL
-  if (currentUrl && currentUrl !== 'about:blank') {
-    const url = new URL(currentUrl);
-    return `${url.protocol}//${url.host}`;
-  }
-
-  // Fallback to environment variable (used in CI) or localhost (local dev)
-  return process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8088';
-}
-
-/**
- * Makes an authenticated API request to Superset with CSRF token handling.
- * Follows the same pattern as Cypress API requests.
- *
- * @param context - Playwright API request context
- * @param page - Playwright page object (for CSRF token extraction and base URL)
- * @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
- * @param endpoint - API endpoint path (e.g., 'api/v1/dataset/')
- * @param data - Optional request body data
- * @param options - Additional fetch options
- * @returns Response object
- */
-export async function apiRequest(
-  context: APIRequestContext,
-  page: Page,
-  method: string,
-  endpoint: string,
-  data?: Record<string, unknown>,
-  options?: {
-    headers?: Record<string, string>;
-    params?: Record<string, string>;
-  },
-) {
-  const csrfToken = await getCSRFToken(page);
-  const baseURL = getBaseURL(page);
-
-  // Construct full URL
-  const url = endpoint.startsWith('http') ? endpoint : `${baseURL}/${endpoint}`;
-
-  // Prepare headers with CSRF token
-  const headers = {
-    'Content-Type': 'application/json',
-    'X-CSRFToken': csrfToken,
-    Cookie: `csrf_access_token=${csrfToken}`,
-    Referer: `${baseURL}/`,
-    ...options?.headers,
-  };
-
-  return context.fetch(url, {
-    method,
-    headers,
-    data: data ? JSON.stringify(data) : undefined,
-    params: options?.params,
-  });
-}
-
-/**
- * Helper for GET requests
- */
-export async function apiGet(
-  context: APIRequestContext,
-  page: Page,
-  endpoint: string,
-  params?: Record<string, string>,
-) {
-  return apiRequest(context, page, 'GET', endpoint, undefined, { params });
-}
-
-/**
- * Helper for POST requests
- */
-export async function apiPost(
-  context: APIRequestContext,
-  page: Page,
-  endpoint: string,
-  data?: Record<string, unknown>,
-) {
-  return apiRequest(context, page, 'POST', endpoint, data);
-}
-
-/**
- * Helper for PUT requests
- */
-export async function apiPut(
-  context: APIRequestContext,
-  page: Page,
-  endpoint: string,
-  data?: Record<string, unknown>,
-) {
-  return apiRequest(context, page, 'PUT', endpoint, data);
-}
-
-/**
- * Helper for DELETE requests
- */
-export async function apiDelete(
-  context: APIRequestContext,
-  page: Page,
-  endpoint: string,
-) {
-  return apiRequest(context, page, 'DELETE', endpoint);
-}
diff --git a/superset-frontend/playwright/helpers/api/database.factories.ts b/superset-frontend/playwright/helpers/api/database.factories.ts
new file mode 100644
index 0000000000..7e1ede2855
--- /dev/null
+++ b/superset-frontend/playwright/helpers/api/database.factories.ts
@@ -0,0 +1,81 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Page } from '@playwright/test';
+import { apiPostDatabase } from './database';
+
+// Public Google Sheets URL for testing
+const NETFLIX_TITLES_SHEET =
+  'https://docs.google.com/spreadsheets/d/19XNqckHGKGGPh83JGFdFGP4Bw9gdXeujq5EoIGwttdM/edit#gid=347941303';
+
+/**
+ * Create a Google Sheets database connection for testing
+ * Uses a public Netflix titles dataset by default
+ * @param page - Playwright page instance
+ * @param databaseName - Name for the database connection
+ * @param tableName - Name for the table/dataset
+ * @returns Database ID from the created database
+ */
+export async function createGsheetsDatabase(
+  page: Page,
+  databaseName: string,
+  tableName: string,
+): Promise<number> {
+  const requestBody = {
+    database_name: databaseName,
+    engine: 'gsheets',
+    configuration_method: 'dynamic_form',
+    engine_information: {
+      disable_ssh_tunneling: true,
+      supports_dynamic_catalog: false,
+      supports_file_upload: true,
+      supports_oauth2: true,
+    },
+    driver: 'apsw',
+    sqlalchemy_uri_placeholder: 'gsheets://',
+    extra: `{"allows_virtual_table_explore":true,"engine_params":{"catalog":{"${tableName}":"${NETFLIX_TITLES_SHEET}"}}}`,
+    expose_in_sqllab: true,
+    catalog: [
+      {
+        name: tableName,
+        value: NETFLIX_TITLES_SHEET,
+      },
+    ],
+    parameters: {
+      service_account_info: '',
+      catalog: {
+        [tableName]: NETFLIX_TITLES_SHEET,
+      },
+    },
+    masked_encrypted_extra: '{}',
+    impersonate_user: true,
+  };
+
+  const response = await apiPostDatabase(page, requestBody);
+
+  if (!response.ok()) {
+    const errorText = await response.text();
+    throw new Error(
+      `Failed to create database: ${response.status()} ${response.statusText()}\n${errorText}`,
+    );
+  }
+
+  const body = await response.json();
+  return body.id;
+}
diff --git a/superset-frontend/playwright/helpers/api/database.ts b/superset-frontend/playwright/helpers/api/database.ts
new file mode 100644
index 0000000000..5a7feb2e9b
--- /dev/null
+++ b/superset-frontend/playwright/helpers/api/database.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Page, APIResponse } from '@playwright/test';
+import { apiPost, apiDelete, ApiRequestOptions } from './requests';
+
+const ENDPOINTS = {
+  DATABASE: '/api/v1/database/',
+} as const;
+
+/**
+ * POST request to create a database connection
+ * @param page - Playwright page instance (provides authentication context)
+ * @param requestBody - Database configuration object
+ * @returns API response from database creation
+ */
+export async function apiPostDatabase(
+  page: Page,
+  requestBody: object,
+): Promise<APIResponse> {
+  return apiPost(page, ENDPOINTS.DATABASE, requestBody);
+}
+
+/**
+ * DELETE request to remove a database connection
+ * @param page - Playwright page instance (provides authentication context)
+ * @param databaseId - ID of the database to delete
+ * @returns API response from database deletion
+ */
+export async function apiDeleteDatabase(
+  page: Page,
+  databaseId: number,
+  options?: ApiRequestOptions,
+): Promise<APIResponse> {
+  return apiDelete(page, `${ENDPOINTS.DATABASE}${databaseId}`, options);
+}
diff --git a/superset-frontend/playwright/helpers/api/dataset.factories.ts b/superset-frontend/playwright/helpers/api/dataset.factories.ts
new file mode 100644
index 0000000000..6150ab2a0e
--- /dev/null
+++ b/superset-frontend/playwright/helpers/api/dataset.factories.ts
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Page } from '@playwright/test';
+import { createGsheetsDatabase } from './database.factories';
+import { apiPostDataset } from './dataset';
+
+/**
+ * Create a test dataset with Google Sheets database
+ * Creates both the database connection and dataset in one call
+ * @param page - Playwright page instance
+ * @param datasetName - Name for the dataset/table
+ * @returns Object containing database ID and dataset ID
+ */
+export async function createTestDataset(
+  page: Page,
+  datasetName: string,
+): Promise<{ dbId: number; datasetId: number }> {
+  // Step 1: Create Google Sheets database with catalog entry
+  // The tableName in the catalog must match the table_name used when creating the dataset
+  const dbName = `test_db_${Date.now()}`;
+  const tableName = datasetName; // Use same name for catalog entry and dataset table_name
+  const dbId = await createGsheetsDatabase(page, dbName, tableName);
+
+  // Step 2: Create dataset using the database
+  // For Google Sheets, table_name must reference the catalog entry name
+  // catalog: null is required to avoid OAuth validation issues
+  const datasetRequestBody = {
+    database: dbId,
+    catalog: null,
+    schema: 'main',
+    table_name: tableName, // Must match the catalog entry name
+  };
+
+  const response = await apiPostDataset(page, datasetRequestBody);
+
+  if (!response.ok()) {
+    const errorText = await response.text();
+    throw new Error(
+      `Failed to create dataset: ${response.status()} ${response.statusText()}\n${errorText}`,
+    );
+  }
+
+  const body = await response.json();
+  const datasetId = body.id;
+
+  return { dbId, datasetId };
+}
diff --git a/superset-frontend/playwright/helpers/api/dataset.ts b/superset-frontend/playwright/helpers/api/dataset.ts
new file mode 100644
index 0000000000..fcf8bee8a8
--- /dev/null
+++ b/superset-frontend/playwright/helpers/api/dataset.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Page, APIResponse } from '@playwright/test';
+import { apiPost, apiDelete, ApiRequestOptions } from './requests';
+
+const ENDPOINTS = {
+  DATASET: '/api/v1/dataset/',
+} as const;
+
+/**
+ * POST request to create a dataset
+ * @param page - Playwright page instance (provides authentication context)
+ * @param requestBody - Dataset configuration object (database, schema, table_name)
+ * @returns API response from dataset creation
+ */
+export async function apiPostDataset(
+  page: Page,
+  requestBody: object,
+): Promise<APIResponse> {
+  return apiPost(page, ENDPOINTS.DATASET, requestBody);
+}
+
+/**
+ * DELETE request to remove a dataset
+ * @param page - Playwright page instance (provides authentication context)
+ * @param datasetId - ID of the dataset to delete
+ * @returns API response from dataset deletion
+ */
+export async function apiDeleteDataset(
+  page: Page,
+  datasetId: number,
+  options?: ApiRequestOptions,
+): Promise<APIResponse> {
+  return apiDelete(page, `${ENDPOINTS.DATASET}${datasetId}`, options);
+}
diff --git a/superset-frontend/playwright/helpers/api/datasets.ts b/superset-frontend/playwright/helpers/api/datasets.ts
deleted file mode 100644
index c663400fc6..0000000000
--- a/superset-frontend/playwright/helpers/api/datasets.ts
+++ /dev/null
@@ -1,272 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { APIRequestContext, Page } from '@playwright/test';
-import { apiGet, apiDelete, apiPost } from './client';
-import { TIMEOUT } from '../../utils/constants';
-
-/**
- * Filter operators for dataset queries
- */
-export interface DatasetFilter {
-  eq?: string;
-  sw?: string;
-  // Add other operators as needed
-}
-
-/**
- * Filters for dataset list queries
- */
-export interface DatasetListFilters {
-  table_name?: DatasetFilter;
-  database?: DatasetFilter;
-  schema?: DatasetFilter;
-  // Add other filterable fields as needed
-}
-
-/**
- * Dataset configuration for creation
- */
-export interface DatasetConfig {
-  databaseId: number; // Numeric database ID (required by API)
-  schema?: string;
-  table_name: string;
-  dataset_name?: string;
-}
-
-/**
- * Dataset response from API
- */
-export interface Dataset {
-  id: number;
-  table_name: string;
-  database: {
-    id: number;
-    database_name: string;
-  };
-  schema?: string;
-  sql?: string;
-  owners?: Array<{ id: number; username: string }>;
-  changed_on_delta_humanized?: string;
-  explore_url?: string;
-}
-
-/**
- * Lists datasets from the Superset API.
- *
- * @param context - API request context
- * @param page - Playwright page object
- * @param filters - Optional filters (e.g., { table_name: { sw: 'test_' } })
- * @returns Array of datasets
- */
-export async function listDatasets(
-  context: APIRequestContext,
-  page: Page,
-  filters?: DatasetListFilters,
-): Promise<Dataset[]> {
-  const params: Record<string, string> = {};
-
-  if (filters) {
-    params.q = JSON.stringify({ filters });
-  }
-
-  const response = await apiGet(context, page, 'api/v1/dataset/', params);
-
-  if (!response.ok()) {
-    throw new Error(
-      `Failed to list datasets: ${response.status()} ${await response.text()}`,
-    );
-  }
-
-  const data = await response.json();
-  return data.result || [];
-}
-
-/**
- * Deletes a dataset by ID.
- *
- * @param context - API request context
- * @param page - Playwright page object
- * @param datasetId - Dataset ID to delete
- * @returns true if deletion was successful
- */
-export async function deleteDataset(
-  context: APIRequestContext,
-  page: Page,
-  datasetId: number,
-): Promise<boolean> {
-  const response = await apiDelete(
-    context,
-    page,
-    `api/v1/dataset/${datasetId}`,
-  );
-
-  if (!response.ok()) {
-    const text = await response.text();
-    console.warn(
-      `Failed to delete dataset ${datasetId}: ${response.status()} ${text}`,
-    );
-    return false;
-  }
-
-  return true;
-}
-
-/**
- * Deletes a dataset by name (table_name).
- * Finds the dataset by name first, then deletes it.
- *
- * @param context - API request context
- * @param page - Playwright page object
- * @param datasetName - Dataset name to delete
- * @returns true if deletion was successful
- */
-export async function deleteDatasetByName(
-  context: APIRequestContext,
-  page: Page,
-  datasetName: string,
-): Promise<boolean> {
-  const datasets = await listDatasets(context, page, {
-    table_name: { eq: datasetName },
-  });
-
-  if (datasets.length === 0) {
-    console.log(`Dataset '${datasetName}' not found, skipping deletion`);
-    return true; // Not found is considered successful cleanup
-  }
-
-  // Delete all matching datasets (in case of duplicates)
-  const deletePromises = datasets.map(dataset =>
-    deleteDataset(context, page, dataset.id),
-  );
-
-  const results = await Promise.all(deletePromises);
-  return results.every(result => result);
-}
-
-/**
- * Cleans up test datasets matching a pattern.
- * Useful for beforeEach hooks to ensure clean state.
- *
- * @param context - API request context
- * @param page - Playwright page object
- * @param pattern - SQL LIKE pattern (e.g., 'test_dataset_%')
- * @returns Number of datasets deleted
- */
-export async function cleanTestDatasets(
-  context: APIRequestContext,
-  page: Page,
-  pattern: string,
-): Promise<number> {
-  // Convert SQL LIKE pattern to filter
-  // Pattern 'test_dataset_%' becomes starts with 'test_dataset_'
-  const prefix = pattern.replace(/%$/, '');
-
-  const datasets = await listDatasets(context, page, {
-    table_name: { sw: prefix }, // 'sw' = starts with
-  });
-
-  if (datasets.length === 0) {
-    return 0;
-  }
-
-  console.log(
-    `Cleaning ${datasets.length} test dataset(s) matching '${pattern}'`,
-  );
-
-  const deletePromises = datasets.map(dataset =>
-    deleteDataset(context, page, dataset.id),
-  );
-
-  const results = await Promise.all(deletePromises);
-  const successCount = results.filter(result => result).length;
-
-  console.log(
-    `Successfully cleaned ${successCount}/${datasets.length} test datasets`,
-  );
-
-  return successCount;
-}
-
-/**
- * Creates a new dataset.
- *
- * @param context - API request context
- * @param page - Playwright page object
- * @param config - Dataset configuration
- * @returns Created dataset ID
- */
-export async function createDataset(
-  context: APIRequestContext,
-  page: Page,
-  config: DatasetConfig,
-): Promise<number> {
-  const payload = {
-    database: config.databaseId, // Numeric ID (required by API)
-    schema: config.schema,
-    table_name: config.table_name,
-    dataset_name: config.dataset_name || config.table_name,
-  };
-
-  const response = await apiPost(context, page, 'api/v1/dataset/', payload);
-
-  if (!response.ok()) {
-    throw new Error(
-      `Failed to create dataset: ${response.status()} ${await response.text()}`,
-    );
-  }
-
-  const data = await response.json();
-  return data.id;
-}
-
-/**
- * Waits for a dataset to appear in the list with the given name.
- * Useful after creation operations to ensure backend processing is complete.
- *
- * @param context - API request context
- * @param page - Playwright page object
- * @param datasetName - Dataset name to wait for
- * @param options - Wait options
- * @returns true if dataset found
- */
-export async function waitForDataset(
-  context: APIRequestContext,
-  page: Page,
-  datasetName: string,
-  options?: { timeout?: number; interval?: number },
-): Promise<boolean> {
-  const timeout = options?.timeout || TIMEOUT.API_POLL_TIMEOUT;
-  const interval = options?.interval || TIMEOUT.API_POLL_INTERVAL;
-  const startTime = Date.now();
-
-  while (Date.now() - startTime < timeout) {
-    const datasets = await listDatasets(context, page, {
-      table_name: { eq: datasetName },
-    });
-
-    if (datasets.length > 0) {
-      return true;
-    }
-
-    await new Promise(resolve => setTimeout(resolve, interval));
-  }
-
-  return false;
-}
diff --git a/superset-frontend/playwright/helpers/api/requests.ts b/superset-frontend/playwright/helpers/api/requests.ts
new file mode 100644
index 0000000000..66e91e39c6
--- /dev/null
+++ b/superset-frontend/playwright/helpers/api/requests.ts
@@ -0,0 +1,178 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Page, APIResponse } from '@playwright/test';
+
+export interface ApiRequestOptions {
+  headers?: Record<string, string>;
+  params?: Record<string, string>;
+  failOnStatusCode?: boolean;
+}
+
+/**
+ * Get base URL for Referer header
+ * Reads from environment variable configured in playwright.config.ts
+ * Preserves full base URL including path prefix (e.g., /app/prefix)
+ */
+function getBaseUrl(_page: Page): string {
+  // Use environment variable which includes path prefix if configured
+  // This matches playwright.config.ts baseURL setting exactly
+  return process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8088';
+}
+
+/**
+ * Get CSRF token from the API endpoint
+ * Superset provides a CSRF token via /api/v1/security/csrf_token/
+ * The session cookie is automatically included by page.request
+ */
+async function getCsrfToken(page: Page): Promise<string> {
+  try {
+    const response = await page.request.get('/api/v1/security/csrf_token/', {
+      failOnStatusCode: false,
+    });
+
+    if (!response.ok()) {
+      console.warn('[CSRF] Failed to fetch CSRF token:', response.status());
+      return '';
+    }
+
+    const json = await response.json();
+    return json.result || '';
+  } catch (error) {
+    console.warn('[CSRF] Error fetching CSRF token:', error);
+    return '';
+  }
+}
+
+/**
+ * Build headers for mutation requests (POST, PUT, PATCH, DELETE)
+ * Includes CSRF token and Referer for Flask-WTF CSRFProtect
+ */
+async function buildHeaders(
+  page: Page,
+  options?: ApiRequestOptions,
+): Promise<Record<string, string>> {
+  const csrfToken = await getCsrfToken(page);
+  const headers: Record<string, string> = {
+    'Content-Type': 'application/json',
+    ...options?.headers,
+  };
+
+  // Include CSRF token and Referer for Flask-WTF CSRFProtect
+  if (csrfToken) {
+    headers['X-CSRFToken'] = csrfToken;
+    headers['Referer'] = getBaseUrl(page);
+  }
+
+  return headers;
+}
+
+/**
+ * Send a GET request
+ * Uses page.request to automatically include browser authentication
+ */
+export async function apiGet(
+  page: Page,
+  url: string,
+  options?: ApiRequestOptions,
+): Promise<APIResponse> {
+  return page.request.get(url, {
+    headers: options?.headers,
+    params: options?.params,
+    failOnStatusCode: options?.failOnStatusCode ?? true,
+  });
+}
+
+/**
+ * Send a POST request
+ * Uses page.request to automatically include browser authentication
+ */
+export async function apiPost(
+  page: Page,
+  url: string,
+  data?: unknown,
+  options?: ApiRequestOptions,
+): Promise<APIResponse> {
+  const headers = await buildHeaders(page, options);
+
+  return page.request.post(url, {
+    data,
+    headers,
+    params: options?.params,
+    failOnStatusCode: options?.failOnStatusCode ?? true,
+  });
+}
+
+/**
+ * Send a PUT request
+ * Uses page.request to automatically include browser authentication
+ */
+export async function apiPut(
+  page: Page,
+  url: string,
+  data?: unknown,
+  options?: ApiRequestOptions,
+): Promise<APIResponse> {
+  const headers = await buildHeaders(page, options);
+
+  return page.request.put(url, {
+    data,
+    headers,
+    params: options?.params,
+    failOnStatusCode: options?.failOnStatusCode ?? true,
+  });
+}
+
+/**
+ * Send a PATCH request
+ * Uses page.request to automatically include browser authentication
+ */
+export async function apiPatch(
+  page: Page,
+  url: string,
+  data?: unknown,
+  options?: ApiRequestOptions,
+): Promise<APIResponse> {
+  const headers = await buildHeaders(page, options);
+
+  return page.request.patch(url, {
+    data,
+    headers,
+    params: options?.params,
+    failOnStatusCode: options?.failOnStatusCode ?? true,
+  });
+}
+
+/**
+ * Send a DELETE request
+ * Uses page.request to automatically include browser authentication
+ */
+export async function apiDelete(
+  page: Page,
+  url: string,
+  options?: ApiRequestOptions,
+): Promise<APIResponse> {
+  const headers = await buildHeaders(page, options);
+
+  return page.request.delete(url, {
+    headers,
+    params: options?.params,
+    failOnStatusCode: options?.failOnStatusCode ?? true,
+  });
+}
diff --git a/superset-frontend/playwright/pages/DatasetListPage.ts b/superset-frontend/playwright/pages/DatasetListPage.ts
index 3f2a516a22..a7b6af75a1 100644
--- a/superset-frontend/playwright/pages/DatasetListPage.ts
+++ b/superset-frontend/playwright/pages/DatasetListPage.ts
@@ -30,6 +30,9 @@ export class DatasetListPage {
 
   private static readonly SELECTORS = {
     DATASET_LINK: '[data-test="internal-link"]',
+    DELETE_ACTION: '.action-button svg[data-icon="delete"]',
+    EXPORT_ACTION: '.action-button svg[data-icon="upload"]',
+    DUPLICATE_ACTION: '.action-button svg[data-icon="copy"]',
   } as const;
 
   constructor(page: Page) {
@@ -76,4 +79,37 @@ export class DatasetListPage {
       DatasetListPage.SELECTORS.DATASET_LINK,
     );
   }
+
+  /**
+   * Clicks the delete action button for a dataset
+   * @param datasetName - The name of the dataset to delete
+   */
+  async clickDeleteAction(datasetName: string): Promise<void> {
+    await this.table.clickRowAction(
+      datasetName,
+      DatasetListPage.SELECTORS.DELETE_ACTION,
+    );
+  }
+
+  /**
+   * Clicks the export action button for a dataset
+   * @param datasetName - The name of the dataset to export
+   */
+  async clickExportAction(datasetName: string): Promise<void> {
+    await this.table.clickRowAction(
+      datasetName,
+      DatasetListPage.SELECTORS.EXPORT_ACTION,
+    );
+  }
+
+  /**
+   * Clicks the duplicate action button for a dataset (virtual datasets only)
+   * @param datasetName - The name of the dataset to duplicate
+   */
+  async clickDuplicateAction(datasetName: string): Promise<void> {
+    await this.table.clickRowAction(
+      datasetName,
+      DatasetListPage.SELECTORS.DUPLICATE_ACTION,
+    );
+  }
 }
diff --git a/superset-frontend/playwright/tests/dataset/dataset-list.spec.ts b/superset-frontend/playwright/tests/dataset/dataset-list.spec.ts
index e1f7a17e29..f2a1a8dd02 100644
--- a/superset-frontend/playwright/tests/dataset/dataset-list.spec.ts
+++ b/superset-frontend/playwright/tests/dataset/dataset-list.spec.ts
@@ -17,23 +17,60 @@
  * under the License.
  */
 
-import { test, expect } from '@playwright/test';
+import { test, expect, Page } from '@playwright/test';
 import { DatasetListPage } from '../../pages/DatasetListPage';
 import { ExplorePage } from '../../pages/ExplorePage';
+import { DeleteConfirmationModal } from '../../components/modals/DeleteConfirmationModal';
+import { Toast } from '../../components/core/Toast';
+import { createTestDataset } from '../../helpers/api/dataset.factories';
+import { apiDeleteDataset } from '../../helpers/api/dataset';
+import { apiDeleteDatabase } from '../../helpers/api/database';
 
 test.describe('Dataset List', () => {
   let datasetListPage: DatasetListPage;
   let explorePage: ExplorePage;
+  let testResources: { datasetId?: number; dbId?: number } = {};
 
   test.beforeEach(async ({ page }) => {
     datasetListPage = new DatasetListPage(page);
     explorePage = new ExplorePage(page);
+    testResources = {}; // Reset for each test
 
     // Navigate to dataset list page
     await datasetListPage.goto();
     await datasetListPage.waitForTableLoad();
   });
 
+  test.afterEach(async ({ page }) => {
+    // Cleanup any resources created during the test
+    await cleanupTestAssets(page, testResources);
+  });
+
+  function cleanupTestAssets(
+    page: Page,
+    resources: { datasetId?: number; dbId?: number },
+  ) {
+    const promises = [];
+
+    if (resources.datasetId) {
+      promises.push(
+        apiDeleteDataset(page, resources.datasetId, {
+          failOnStatusCode: false,
+        }).catch(() => {}),
+      );
+    }
+
+    if (resources.dbId) {
+      promises.push(
+        apiDeleteDatabase(page, resources.dbId, {
+          failOnStatusCode: false,
+        }).catch(() => {}),
+      );
+    }
+
+    return Promise.all(promises);
+  }
+
   test('should navigate to Explore when dataset name is clicked', async () => {
     // Test uses existing fixture dataset 'birth_names'
     // (available in Superset examples database)
@@ -55,4 +92,42 @@ test.describe('Dataset List', () => {
     await expect(explorePage.getVizSwitcher()).toBeVisible();
     await expect(explorePage.getVizSwitcher()).toContainText('Table');
   });
+
+  test('should delete a dataset with confirmation', async ({ page }) => {
+    // Create test dataset
+    const datasetName = `test_delete_${Date.now()}`;
+    testResources = await createTestDataset(page, datasetName);
+
+    // Refresh page to see new dataset
+    await datasetListPage.goto();
+    await datasetListPage.waitForTableLoad();
+
+    // Verify dataset is visible in list
+    await expect(datasetListPage.getDatasetRow(datasetName)).toBeVisible();
+
+    // Click delete action button
+    await datasetListPage.clickDeleteAction(datasetName);
+
+    // Delete confirmation modal should appear
+    const deleteModal = new DeleteConfirmationModal(page);
+    await deleteModal.waitForVisible();
+
+    // Type "DELETE" to confirm
+    await deleteModal.fillConfirmationInput('DELETE');
+
+    // Click the Delete button
+    await deleteModal.clickDelete();
+
+    // Modal should close
+    await deleteModal.waitForHidden();
+
+    // Verify success toast appears with correct message
+    const toast = new Toast(page);
+    const successToast = toast.getSuccess();
+    await expect(successToast).toBeVisible();
+    await expect(toast.getMessage()).toContainText('Deleted');
+
+    // Verify dataset is removed from list
+    await expect(datasetListPage.getDatasetRow(datasetName)).not.toBeVisible();
+  });
 });