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

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

commit e1e40bdc10bd797d8dd20e572edc1a32dc68f16c
Author: Joe Li <[email protected]>
AuthorDate: Sat Nov 15 10:09:32 2025 -0800

    test: add comprehensive React Testing Library and integration tests for 
DatasetList
    
    Add complete test coverage for the DatasetList page including:
    - Component tests: DatasetList main, behaviors, ListView, permissions, 
DuplicateDatasetModal
    - Integration tests: multi-component orchestration and hook-level state 
management
    - Test helpers: shared fixtures and utilities
    
    Tests breakdown:
    - DatasetList.test.tsx: 28 component tests
    - DatasetList.behavior.test.tsx: 10 behavior tests
    - DatasetList.listview.test.tsx: 36 ListView tests
    - DatasetList.permissions.test.tsx: 14 permission tests
    - DuplicateDatasetModal.test.tsx: 9 modal tests
    - DatasetList.integration.test.tsx: 2 integration tests
    
    Total: 99 tests (97 component + 2 integration)
    
    Includes fixes for:
    - antd 5.27.6 compatibility (semantic selectors)
    - Async cleanup patterns for reliable test execution
    - Test timing and synchronization issues
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <[email protected]>
---
 .../datasets/DuplicateDatasetModal.test.tsx        |  282 ++++
 .../DatasetList/DatasetList.behavior.test.tsx      |  485 +++++++
 .../DatasetList/DatasetList.integration.test.tsx   |  211 +++
 .../DatasetList/DatasetList.listview.test.tsx      | 1484 ++++++++++++++++++++
 .../DatasetList/DatasetList.permissions.test.tsx   |  394 ++++++
 .../src/pages/DatasetList/DatasetList.test.tsx     |  528 +++++++
 .../pages/DatasetList/DatasetList.testHelpers.tsx  |  539 +++++++
 7 files changed, 3923 insertions(+)

diff --git 
a/superset-frontend/src/features/datasets/DuplicateDatasetModal.test.tsx 
b/superset-frontend/src/features/datasets/DuplicateDatasetModal.test.tsx
new file mode 100644
index 0000000000..849bf5f9ad
--- /dev/null
+++ b/superset-frontend/src/features/datasets/DuplicateDatasetModal.test.tsx
@@ -0,0 +1,282 @@
+/**
+ * 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 { render, screen, waitFor } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { ThemeProvider, supersetTheme } from '@apache-superset/core';
+import DuplicateDatasetModal from './DuplicateDatasetModal';
+
+// Test-only fixture type that includes all fields from API responses
+// Matches VirtualDataset structure from DatasetList but defined locally for 
tests
+interface VirtualDatasetFixture {
+  id: number;
+  table_name: string;
+  kind: string;
+  schema: string;
+  database: {
+    id: string;
+    database_name: string;
+  };
+  owners: Array<{ first_name: string; last_name: string; id: number }>;
+  changed_by_name: string;
+  changed_by: string;
+  changed_on_delta_humanized: string;
+  explore_url: string;
+  extra: string;
+  sql: string | null;
+}
+
+// Test fixture with extra/sql fields that exist in actual API responses
+const mockDataset: VirtualDatasetFixture = {
+  id: 1,
+  table_name: 'original_dataset',
+  kind: 'virtual',
+  schema: 'public',
+  database: {
+    id: '1',
+    database_name: 'PostgreSQL',
+  },
+  owners: [],
+  changed_by_name: 'Admin',
+  changed_by: 'Admin User',
+  changed_on_delta_humanized: '1 day ago',
+  explore_url: '/explore/?datasource=1__table',
+  extra: '{}',
+  sql: 'SELECT * FROM table',
+};
+
+const Wrapper = ({
+  dataset,
+  onHide,
+  onDuplicate,
+}: {
+  dataset: VirtualDatasetFixture | null;
+  onHide: jest.Mock;
+  onDuplicate: jest.Mock;
+}) => (
+  <ThemeProvider theme={supersetTheme}>
+    <DuplicateDatasetModal
+      dataset={dataset}
+      onHide={onHide}
+      onDuplicate={onDuplicate}
+    />
+  </ThemeProvider>
+);
+
+const renderModal = (
+  dataset: VirtualDatasetFixture | null,
+  onHide: jest.Mock,
+  onDuplicate: jest.Mock,
+) =>
+  render(
+    <Wrapper dataset={dataset} onHide={onHide} onDuplicate={onDuplicate} />,
+  );
+
+test('modal opens when dataset is provided', async () => {
+  const onHide = jest.fn();
+  const onDuplicate = jest.fn();
+
+  renderModal(mockDataset, onHide, onDuplicate);
+
+  // Modal should be visible
+  expect(await screen.findByText('Duplicate dataset')).toBeInTheDocument();
+
+  // Input field should be present
+  expect(screen.getByTestId('duplicate-modal-input')).toBeInTheDocument();
+
+  // Duplicate button should be present
+  expect(
+    screen.getByRole('button', { name: /duplicate/i }),
+  ).toBeInTheDocument();
+});
+
+test('modal does not open when dataset is null', () => {
+  const onHide = jest.fn();
+  const onDuplicate = jest.fn();
+
+  renderModal(null, onHide, onDuplicate);
+
+  // Modal should not be visible
+  expect(screen.queryByText('Duplicate dataset')).not.toBeInTheDocument();
+});
+
+test('duplicate button disabled after clearing input', async () => {
+  const onHide = jest.fn();
+  const onDuplicate = jest.fn();
+
+  renderModal(mockDataset, onHide, onDuplicate);
+
+  const input = await screen.findByTestId('duplicate-modal-input');
+
+  // Type some text first
+  await userEvent.type(input, 'test');
+
+  // Then clear it
+  await userEvent.clear(input);
+
+  // Duplicate button should now be disabled (empty input)
+  const duplicateButton = screen.getByRole('button', { name: /duplicate/i });
+  expect(duplicateButton).toBeDisabled();
+});
+
+test('duplicate button enabled when name is entered', async () => {
+  const onHide = jest.fn();
+  const onDuplicate = jest.fn();
+
+  renderModal(mockDataset, onHide, onDuplicate);
+
+  const input = await screen.findByTestId('duplicate-modal-input');
+
+  // Type a new name
+  await userEvent.type(input, 'new_dataset_copy');
+
+  // Duplicate button should now be enabled
+  const duplicateButton = await screen.findByRole('button', {
+    name: /duplicate/i,
+  });
+  expect(duplicateButton).toBeEnabled();
+});
+
+test('clicking Duplicate calls onDuplicate with new name', async () => {
+  const onHide = jest.fn();
+  const onDuplicate = jest.fn();
+
+  renderModal(mockDataset, onHide, onDuplicate);
+
+  const input = await screen.findByTestId('duplicate-modal-input');
+
+  // Type a new name
+  await userEvent.type(input, 'new_dataset_copy');
+
+  // Click Duplicate button
+  const duplicateButton = await screen.findByRole('button', {
+    name: /duplicate/i,
+  });
+  await userEvent.click(duplicateButton);
+
+  // onDuplicate should be called with the new name
+  await waitFor(() => {
+    expect(onDuplicate).toHaveBeenCalledWith('new_dataset_copy');
+  });
+});
+
+test('pressing Enter key triggers duplicate action', async () => {
+  const onHide = jest.fn();
+  const onDuplicate = jest.fn();
+
+  renderModal(mockDataset, onHide, onDuplicate);
+
+  const input = await screen.findByTestId('duplicate-modal-input');
+
+  // Clear any existing value and type new name with Enter at end
+  await userEvent.clear(input);
+  await userEvent.type(input, 'new_dataset_copy{enter}');
+
+  // onDuplicate should be called by onPressEnter handler
+  await waitFor(() => {
+    expect(onDuplicate).toHaveBeenCalledWith('new_dataset_copy');
+  });
+});
+
+test('modal closes when onHide is called', async () => {
+  const onHide = jest.fn();
+  const onDuplicate = jest.fn();
+
+  const { rerender } = renderModal(mockDataset, onHide, onDuplicate);
+
+  expect(await screen.findByText('Duplicate dataset')).toBeInTheDocument();
+
+  // Simulate closing the modal by setting dataset to null
+  rerender(
+    <Wrapper dataset={null} onHide={onHide} onDuplicate={onDuplicate} />,
+  );
+
+  // Modal should no longer be visible (Ant Design keeps it in DOM but hides 
it)
+  await waitFor(() => {
+    expect(screen.queryByText('Duplicate dataset')).not.toBeVisible();
+  });
+});
+
+test('cancel button clears input and closes modal', async () => {
+  const onHide = jest.fn();
+  const onDuplicate = jest.fn();
+
+  const { rerender } = renderModal(mockDataset, onHide, onDuplicate);
+
+  const input = await screen.findByTestId('duplicate-modal-input');
+
+  // Type some text
+  await userEvent.type(input, 'test_name');
+
+  expect(input).toHaveValue('test_name');
+
+  // Click cancel button
+  const cancelButton = await screen.findByRole('button', { name: /cancel/i });
+  await userEvent.click(cancelButton);
+
+  // onHide should be called
+  expect(onHide).toHaveBeenCalled();
+
+  // Simulate closing the modal (parent sets dataset to null)
+  rerender(
+    <Wrapper dataset={null} onHide={onHide} onDuplicate={onDuplicate} />,
+  );
+
+  // Modal should be hidden
+  await waitFor(() => {
+    expect(screen.queryByText('Duplicate dataset')).not.toBeVisible();
+  });
+
+  // Reopen with same dataset - input should be cleared
+  rerender(
+    <Wrapper dataset={mockDataset} onHide={onHide} onDuplicate={onDuplicate} 
/>,
+  );
+
+  const reopenedInput = await screen.findByTestId('duplicate-modal-input');
+  expect(reopenedInput).toHaveValue('');
+});
+
+test('input field clears when new dataset is provided', async () => {
+  const onHide = jest.fn();
+  const onDuplicate = jest.fn();
+
+  const { rerender } = renderModal(mockDataset, onHide, onDuplicate);
+
+  const input = await screen.findByTestId('duplicate-modal-input');
+
+  // Type a name
+  await userEvent.type(input, 'old_name');
+
+  expect(input).toHaveValue('old_name');
+
+  // Switch to different dataset
+  const newDataset: VirtualDatasetFixture = {
+    ...mockDataset,
+    id: 2,
+    table_name: 'different_dataset',
+  };
+
+  rerender(
+    <Wrapper dataset={newDataset} onHide={onHide} onDuplicate={onDuplicate} />,
+  );
+
+  // Input should be cleared
+  await waitFor(() => {
+    expect(input).toHaveValue('');
+  });
+});
diff --git 
a/superset-frontend/src/pages/DatasetList/DatasetList.behavior.test.tsx 
b/superset-frontend/src/pages/DatasetList/DatasetList.behavior.test.tsx
new file mode 100644
index 0000000000..c4cc015100
--- /dev/null
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.behavior.test.tsx
@@ -0,0 +1,485 @@
+/**
+ * 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 { act, cleanup, screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import fetchMock from 'fetch-mock';
+import rison from 'rison';
+import { ComponentType } from 'react';
+import {
+  setupMocks,
+  renderDatasetList,
+  waitForDatasetsPageReady,
+  mockAdminUser,
+  mockDatasets,
+  setupDeleteMocks,
+  mockRelatedCharts,
+  mockRelatedDashboards,
+  mockHandleResourceExport,
+  API_ENDPOINTS,
+} from './DatasetList.testHelpers';
+
+jest.mock('src/utils/export');
+
+// Mock withToasts HOC to be a passthrough so we can spy on toast calls
+jest.mock('src/components/MessageToasts/withToasts', () => ({
+  __esModule: true,
+  default: <P extends object>(Component: ComponentType<P>) => Component,
+}));
+
+beforeEach(() => {
+  setupMocks();
+  jest.clearAllMocks();
+});
+
+afterEach(async () => {
+  // Wait for any pending state updates to complete before cleanup
+  await act(async () => {
+    await new Promise(resolve => setTimeout(resolve, 0));
+  });
+  cleanup();
+  fetchMock.reset();
+  jest.restoreAllMocks();
+});
+
+test('typing in search updates the input value correctly', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('search-filter-container')).toBeInTheDocument();
+  });
+
+  const searchContainer = screen.getByTestId('search-filter-container');
+  const searchInput = within(searchContainer).getByRole('textbox');
+
+  // Type search query
+  await userEvent.type(searchInput, 'sales');
+
+  // Verify input value is updated
+  await waitFor(() => {
+    expect(searchInput).toHaveValue('sales');
+  });
+});
+
+test('typing in search triggers debounced API call with search filter', async 
() => {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('search-filter-container')).toBeInTheDocument();
+  });
+
+  const searchContainer = screen.getByTestId('search-filter-container');
+  const searchInput = within(searchContainer).getByRole('textbox');
+
+  // Record initial API calls
+  const initialCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+
+  // Type search query and submit with Enter to trigger the debounced fetch
+  await userEvent.type(searchInput, 'sales{enter}');
+
+  // Wait for debounced API call
+  await waitFor(
+    () => {
+      const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+      expect(calls.length).toBeGreaterThan(initialCallCount);
+    },
+    { timeout: 5000 },
+  );
+
+  // Verify the latest API call includes search filter in URL
+  const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+  const latestCall = calls[calls.length - 1];
+  const url = latestCall[0] as string;
+
+  // URL should contain filters parameter with search term
+  expect(url).toContain('filters');
+  const risonPayload = url.split('?q=')[1];
+  expect(risonPayload).toBeTruthy();
+  const decoded = rison.decode(decodeURIComponent(risonPayload!)) as Record<
+    string,
+    unknown
+  >;
+  const filters = Array.isArray(decoded?.filters) ? decoded.filters : [];
+  const hasSalesFilter = filters.some(
+    (filter: Record<string, unknown>) =>
+      typeof filter?.value === 'string' &&
+      filter.value.toLowerCase().includes('sales'),
+  );
+  expect(hasSalesFilter).toBe(true);
+});
+
+test('500 error triggers danger toast with error message', async () => {
+  const addDangerToast = jest.fn();
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    {
+      status: 500,
+      body: { message: 'Internal Server Error' },
+    },
+    { overwriteRoutes: true },
+  );
+
+  // Pass toast spy directly via props to bypass withToasts HOC
+  renderDatasetList(mockAdminUser, {
+    addDangerToast,
+    addSuccessToast: jest.fn(),
+  });
+
+  // Verify component renders despite error
+  await waitForDatasetsPageReady();
+
+  // Verify danger toast called with error information
+  await waitFor(
+    () => {
+      expect(addDangerToast).toHaveBeenCalled();
+    },
+    { timeout: 5000 },
+  );
+
+  // Verify toast message contains error keywords
+  expect(addDangerToast.mock.calls.length).toBeGreaterThan(0);
+  const toastMessage = String(addDangerToast.mock.calls[0][0]);
+  expect(
+    toastMessage.includes('error') ||
+      toastMessage.includes('Error') ||
+      toastMessage.includes('500') ||
+      toastMessage.includes('Internal Server'),
+  ).toBe(true);
+});
+
+test('network timeout triggers danger toast', async () => {
+  const addDangerToast = jest.fn();
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { throws: new Error('Network timeout') },
+    { overwriteRoutes: true },
+  );
+
+  // Pass toast spy directly via props to bypass withToasts HOC
+  renderDatasetList(mockAdminUser, {
+    addDangerToast,
+    addSuccessToast: jest.fn(),
+  });
+
+  // Verify component renders despite error
+  await waitForDatasetsPageReady();
+
+  // Verify danger toast called with timeout message
+  await waitFor(
+    () => {
+      expect(addDangerToast).toHaveBeenCalled();
+    },
+    { timeout: 5000 },
+  );
+
+  // Verify toast message contains timeout/network keywords
+  expect(addDangerToast.mock.calls.length).toBeGreaterThan(0);
+  const toastMessage = String(addDangerToast.mock.calls[0][0]);
+  expect(
+    toastMessage.includes('timeout') ||
+      toastMessage.includes('Timeout') ||
+      toastMessage.includes('network') ||
+      toastMessage.includes('Network') ||
+      toastMessage.includes('error'),
+  ).toBe(true);
+});
+
+test('clicking delete opens modal with related objects count', async () => {
+  const datasetToDelete = mockDatasets[0];
+
+  // Set up delete mocks
+  setupDeleteMocks(datasetToDelete.id);
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [datasetToDelete], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  // Wait for dataset to render
+  await waitFor(() => {
+    expect(screen.getByText(datasetToDelete.table_name)).toBeInTheDocument();
+  });
+
+  // Find and click delete button in the row
+  const table = screen.getByTestId('listview-table');
+  const datasetRow = within(table)
+    .getAllByRole('row')
+    .find(row => within(row).queryByText(datasetToDelete.table_name));
+  expect(datasetRow).toBeTruthy();
+  await userEvent.hover(datasetRow!);
+  const deleteButton = within(datasetRow!).getByTestId('delete');
+
+  await userEvent.click(deleteButton);
+
+  // Verify modal opens with related objects
+  const modal = await screen.findByRole('dialog');
+  expect(modal).toBeInTheDocument();
+
+  // Check for related charts count
+  expect(modal).toHaveTextContent(
+    new RegExp(mockRelatedCharts.count.toString()),
+  );
+  // Check for related dashboards count
+  expect(modal).toHaveTextContent(
+    new RegExp(mockRelatedDashboards.count.toString()),
+  );
+});
+
+test('clicking export calls handleResourceExport with dataset ID', async () => 
{
+  const datasetToExport = mockDatasets[0];
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [datasetToExport], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(datasetToExport.table_name)).toBeInTheDocument();
+  });
+
+  // Find and click export button
+  const table = screen.getByTestId('listview-table');
+  const exportButton = await within(table).findByTestId('upload');
+
+  await userEvent.click(exportButton);
+
+  // Verify export was called with correct ID
+  await waitFor(() => {
+    expect(mockHandleResourceExport).toHaveBeenCalledWith(
+      'dataset',
+      [datasetToExport.id],
+      expect.any(Function),
+    );
+  });
+});
+
+test('clicking duplicate opens modal and submits duplicate request', async () 
=> {
+  const datasetToDuplicate = {
+    ...mockDatasets[1],
+    kind: 'virtual',
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [datasetToDuplicate], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  fetchMock.post(
+    API_ENDPOINTS.DATASET_DUPLICATE,
+    { id: 999, table_name: 'Copy of Dataset' },
+    { overwriteRoutes: true },
+  );
+
+  const addSuccessToast = jest.fn();
+
+  renderDatasetList(mockAdminUser, {
+    addDangerToast: jest.fn(),
+    addSuccessToast,
+  });
+
+  await waitFor(() => {
+    
expect(screen.getByText(datasetToDuplicate.table_name)).toBeInTheDocument();
+  });
+
+  // Track initial dataset list API calls BEFORE duplicate action
+  const initialDatasetCallCount = fetchMock.calls(
+    API_ENDPOINTS.DATASETS,
+  ).length;
+
+  const row = screen.getByText(datasetToDuplicate.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  const duplicateIcon = await within(row!).findByTestId('copy');
+  const duplicateButton = duplicateIcon.closest(
+    '[role="button"]',
+  ) as HTMLElement | null;
+  expect(duplicateButton).toBeTruthy();
+
+  await userEvent.click(duplicateButton!);
+
+  const modal = await screen.findByRole('dialog');
+  const modalInput = within(modal).getByRole('textbox');
+  await userEvent.clear(modalInput);
+  await userEvent.type(modalInput, 'Copy of Dataset');
+
+  const confirmButton = within(modal).getByRole('button', {
+    name: /duplicate/i,
+  });
+  await userEvent.click(confirmButton);
+
+  // Verify duplicate API was called with correct payload
+  await waitFor(() => {
+    const calls = fetchMock.calls(API_ENDPOINTS.DATASET_DUPLICATE);
+    expect(calls.length).toBeGreaterThan(0);
+
+    // Verify POST body contains correct dataset info
+    const requestBody = JSON.parse(calls[0][1]?.body as string);
+    expect(requestBody.base_model_id).toBe(datasetToDuplicate.id);
+    expect(requestBody.table_name).toBe('Copy of Dataset');
+  });
+
+  // Verify modal closes after successful duplicate
+  await waitFor(() => {
+    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+  });
+
+  // Verify refreshData() is called (observable via new dataset list API call)
+  await waitFor(
+    () => {
+      const datasetCalls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+      expect(datasetCalls.length).toBeGreaterThan(initialDatasetCallCount);
+    },
+    { timeout: 3000 },
+  );
+
+  // Note: Success toast feature not implemented (see index.tsx:718-721)
+  expect(addSuccessToast).not.toHaveBeenCalled();
+});
+
+test('certified dataset shows badge and tooltip with certification details', 
async () => {
+  const certifiedDataset = {
+    ...mockDatasets[1],
+    extra: JSON.stringify({
+      certification: {
+        certified_by: 'Data Team',
+        details: 'Approved for production use',
+      },
+    }),
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [certifiedDataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(certifiedDataset.table_name)).toBeInTheDocument();
+  });
+
+  // Verify the row renders with the dataset
+  const row = screen.getByText(certifiedDataset.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Find certification badge within the row (fail-fast if not found)
+  const certBadge = await within(row!).findByRole('img', {
+    name: /certified/i,
+  });
+  expect(certBadge).toBeInTheDocument();
+
+  // Hover to reveal tooltip
+  await userEvent.hover(certBadge);
+
+  // Wait for tooltip content to appear
+  const tooltip = await screen.findByRole('tooltip');
+  expect(tooltip).toBeInTheDocument();
+  expect(tooltip).toHaveTextContent(/Data Team/i);
+  expect(tooltip).toHaveTextContent(/Approved for production/i);
+});
+
+test('dataset with warning shows icon and tooltip with markdown content', 
async () => {
+  const warningMessage = 'This dataset contains PII. Handle with care.';
+  const datasetWithWarning = {
+    ...mockDatasets[2],
+    extra: JSON.stringify({
+      warning_markdown: warningMessage,
+    }),
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [datasetWithWarning], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    
expect(screen.getByText(datasetWithWarning.table_name)).toBeInTheDocument();
+  });
+
+  // Verify row exists
+  const row = screen.getByText(datasetWithWarning.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Find warning icon within the row (fail-fast if not found)
+  const warningIcon = await within(row!).findByRole('img', {
+    name: /warning/i,
+  });
+  expect(warningIcon).toBeInTheDocument();
+
+  // Hover to reveal tooltip with markdown content
+  await userEvent.hover(warningIcon);
+
+  // Wait for tooltip to appear with warning text
+  const tooltip = await screen.findByRole('tooltip');
+  expect(tooltip).toBeInTheDocument();
+  expect(tooltip).toHaveTextContent(/PII/i);
+  expect(tooltip).toHaveTextContent(/Handle with care/i);
+});
+
+test('dataset name links to Explore with correct URL and accessible label', 
async () => {
+  const dataset = mockDatasets[0];
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  // Find the dataset row and scope the link query to it
+  const row = screen.getByText(dataset.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Find the internal link within the dataset row (fail-fast if not found)
+  const exploreLink = within(row!).getByTestId('internal-link');
+  expect(exploreLink).toBeInTheDocument();
+
+  // Verify link has correct href to Explore page
+  expect(exploreLink).toHaveAttribute('href', dataset.explore_url);
+  expect(exploreLink).toHaveAttribute(
+    'href',
+    expect.stringContaining('/explore/'),
+  );
+
+  // Verify link contains dataset ID
+  expect(exploreLink).toHaveAttribute(
+    'href',
+    expect.stringContaining(`${dataset.id}__table`),
+  );
+});
+
+// Note: Component "+1" tests for state persistence through operations have 
been
+// moved to DatasetList.listview.test.tsx where they can use the reliable 
selectOption helper.
diff --git 
a/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx 
b/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx
new file mode 100644
index 0000000000..b09c7bc41f
--- /dev/null
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx
@@ -0,0 +1,211 @@
+/**
+ * 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 { cleanup, screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import fetchMock from 'fetch-mock';
+import rison from 'rison';
+import { selectOption } from 'spec/helpers/testing-library';
+import {
+  setupMocks,
+  renderDatasetList,
+  mockAdminUser,
+  mockDatasets,
+  setupBulkDeleteMocks,
+  API_ENDPOINTS,
+} from './DatasetList.testHelpers';
+
+/**
+ * Integration Contract Tests
+ *
+ * These tests verify multi-component orchestration that cannot be tested
+ * in component isolation. Unlike component tests which mock all dependencies,
+ * integration tests use real Redux/React Query/Router state management.
+ *
+ * Only 2 tests are needed here - most workflows are covered by component "+1" 
tests.
+ */
+
+jest.mock('src/utils/export');
+
+beforeEach(() => {
+  setupMocks();
+  jest.clearAllMocks();
+});
+
+afterEach(() => {
+  cleanup();
+  fetchMock.reset();
+  jest.restoreAllMocks();
+});
+
+test('ListView provider correctly merges filter + sort + pagination state on 
refetch', async () => {
+  // This test verifies that when multiple state sources are combined,
+  // the ListView provider correctly merges them for the API call.
+  // Component tests verify individual pieces persist; this verifies they 
COMBINE correctly.
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: mockDatasets, count: mockDatasets.length },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // 1. Apply a sort by clicking Name header
+  const table = screen.getByTestId('listview-table');
+  const nameHeader = within(table).getByRole('columnheader', { name: /Name/i 
});
+
+  await userEvent.click(nameHeader);
+
+  // 2. Apply a filter using selectOption helper
+  const beforeFilterCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+  await selectOption('Virtual', 'Type');
+
+  // Wait for filter API call to complete
+  await waitFor(() => {
+    const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+    expect(calls.length).toBeGreaterThan(beforeFilterCallCount);
+  });
+
+  // 3. Verify the final API call contains ALL three state pieces merged 
correctly
+  const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+  const latestCall = calls[calls.length - 1];
+  const url = latestCall[0] as string;
+
+  // Decode the rison payload
+  const risonPayload = url.split('?q=')[1];
+  expect(risonPayload).toBeTruthy();
+  const decoded = rison.decode(decodeURIComponent(risonPayload!)) as Record<
+    string,
+    unknown
+  >;
+
+  // Verify ALL three pieces of state are present and merged:
+  // 1. Sort (order_column)
+  expect(decoded?.order_column).toBeTruthy();
+
+  // 2. Filter (filters array)
+  const filters = Array.isArray(decoded?.filters) ? decoded.filters : [];
+  const hasTypeFilter = filters.some(
+    (filter: Record<string, unknown>) =>
+      filter?.col === 'sql' && filter?.value === false,
+  );
+  expect(hasTypeFilter).toBe(true);
+
+  // 3. Pagination (page_size is present with default value)
+  expect(decoded?.page_size).toBeTruthy();
+
+  // This confirms ListView provider merges state from multiple sources 
correctly
+});
+
+test('bulk action orchestration: selection → action → cleanup cycle works 
correctly', async () => {
+  // This test verifies the full bulk operation cycle across multiple 
components:
+  // 1. Bulk mode UI (selection state)
+  // 2. Bulk action handler (delete operation)
+  // 3. Selection cleanup (state reset)
+
+  setupBulkDeleteMocks();
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: mockDatasets, count: mockDatasets.length },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // 1. Enter bulk mode and select items
+  const bulkSelectButton = screen.getByRole('button', {
+    name: /bulk select/i,
+  });
+  await userEvent.click(bulkSelectButton);
+
+  await waitFor(() => {
+    const checkboxes = screen.getAllByRole('checkbox');
+    expect(checkboxes.length).toBeGreaterThan(0);
+  });
+
+  // Select first 2 items (skip select-all checkbox at index 0)
+  const checkboxes = screen.getAllByRole('checkbox');
+  await userEvent.click(checkboxes[1]);
+  await userEvent.click(checkboxes[2]);
+
+  // Wait for selections to register - assert on "selected" text which is what 
users see
+  await screen.findByText(/selected/i);
+
+  // 2. Execute bulk delete
+  // Multiple bulk actions share the same test ID, so filter by text content
+  const bulkActionButtons = await screen.findAllByTestId('bulk-select-action');
+  const bulkDeleteButton = bulkActionButtons.find(btn =>
+    btn.textContent?.includes('Delete'),
+  );
+  expect(bulkDeleteButton).toBeTruthy();
+  await userEvent.click(bulkDeleteButton!);
+
+  // Confirm in modal - type DELETE to enable button
+  const modal = await screen.findByRole('dialog');
+  const confirmInput = within(modal).getByTestId('delete-modal-input');
+  await userEvent.clear(confirmInput);
+  await userEvent.type(confirmInput, 'DELETE');
+
+  // Capture datasets call count before confirming
+  const datasetsCallCountBeforeDelete = fetchMock.calls(
+    API_ENDPOINTS.DATASETS,
+  ).length;
+
+  const confirmButton = within(modal)
+    .getAllByRole('button', { name: /^delete$/i })
+    .pop();
+  await userEvent.click(confirmButton!);
+
+  // 3. Wait for bulk delete API call to be made
+  await waitFor(() => {
+    const deleteCalls = fetchMock.calls(API_ENDPOINTS.DATASET_BULK_DELETE);
+    expect(deleteCalls.length).toBeGreaterThan(0);
+  });
+
+  // Wait for modal to close
+  await waitFor(() => {
+    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+  });
+
+  // Wait for datasets refetch after delete
+  await waitFor(() => {
+    const datasetsCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+    expect(datasetsCallCount).toBeGreaterThan(datasetsCallCountBeforeDelete);
+  });
+
+  // 4. Verify selection count shows 0 (selections cleared but still in bulk 
mode)
+  // After bulk delete, items are deselected but bulk mode may remain active
+  await waitFor(() => {
+    expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+      /0 selected/i,
+    );
+  });
+
+  // This confirms the full bulk operation cycle coordinates correctly:
+  // selection state → action handler → list refresh → state cleanup
+});
diff --git 
a/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx 
b/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx
new file mode 100644
index 0000000000..12bf223094
--- /dev/null
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx
@@ -0,0 +1,1484 @@
+/**
+ * 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 { act, cleanup, screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import fetchMock from 'fetch-mock';
+import rison from 'rison';
+import { SupersetClient } from '@superset-ui/core';
+import { selectOption } from 'spec/helpers/testing-library';
+import {
+  setupMocks,
+  renderDatasetList,
+  mockAdminUser,
+  mockDatasets,
+  setupDeleteMocks,
+  setupBulkDeleteMocks,
+  setupDuplicateMocks,
+  mockHandleResourceExport,
+  assertOnlyExpectedCalls,
+  API_ENDPOINTS,
+} from './DatasetList.testHelpers';
+
+const mockAddDangerToast = jest.fn();
+const mockAddSuccessToast = jest.fn();
+
+jest.mock('src/components/MessageToasts/actions', () => ({
+  addDangerToast: (msg: string) => {
+    mockAddDangerToast(msg);
+    return () => ({ type: '@@toast/danger' });
+  },
+  addSuccessToast: (msg: string) => {
+    mockAddSuccessToast(msg);
+    return () => ({ type: '@@toast/success' });
+  },
+}));
+
+jest.mock('src/utils/export');
+
+const buildSupersetClientError = ({
+  status,
+  message,
+}: {
+  status: number;
+  message: string;
+}) => ({
+  message,
+  error: message,
+  status,
+  response: {
+    status,
+    json: async () => ({ message }),
+    text: async () => message,
+    clone() {
+      return {
+        ...this,
+        json: async () => ({ message }),
+        text: async () => message,
+      };
+    },
+  },
+});
+
+/**
+ * Helper to set up error test scenarios with SupersetClient spy
+ * Reduces boilerplate for error toast tests
+ */
+const setupErrorTestScenario = ({
+  dataset,
+  method,
+  endpoint,
+  errorStatus,
+  errorMessage,
+}: {
+  dataset: (typeof mockDatasets)[0];
+  method: 'get' | 'post';
+  endpoint: string;
+  errorStatus: number;
+  errorMessage: string;
+}) => {
+  // Spy on SupersetClient method and throw error for specific endpoint
+  const originalMethod =
+    method === 'get'
+      ? SupersetClient.get.bind(SupersetClient)
+      : SupersetClient.post.bind(SupersetClient);
+
+  jest.spyOn(SupersetClient, method).mockImplementation(async request => {
+    if (request.endpoint?.includes(endpoint)) {
+      throw buildSupersetClientError({
+        status: errorStatus,
+        message: errorMessage,
+      });
+    }
+    return originalMethod(request);
+  });
+
+  // Configure fetchMock to return single dataset
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  // Render component
+  renderDatasetList(mockAdminUser);
+};
+
+beforeEach(() => {
+  setupMocks();
+  jest.clearAllMocks();
+});
+
+afterEach(async () => {
+  // Wait for any pending state updates to complete before cleanup
+  await act(async () => {
+    await new Promise(resolve => setTimeout(resolve, 0));
+  });
+  cleanup();
+  fetchMock.reset();
+  jest.restoreAllMocks();
+});
+
+test('only expected API endpoints are called on initial render', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Verify only expected endpoints were called (no unmocked calls)
+  // These are the minimum required endpoints for initial dataset list render
+  assertOnlyExpectedCalls([
+    API_ENDPOINTS.DATASETS_INFO, // Permission check
+    API_ENDPOINTS.DATASETS, // Main dataset list data
+  ]);
+});
+
+test('renders all required column headers', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+
+  // Verify all column headers are present
+  expect(
+    within(table).getByRole('columnheader', { name: /Name/i }),
+  ).toBeInTheDocument();
+  expect(
+    within(table).getByRole('columnheader', { name: /Type/i }),
+  ).toBeInTheDocument();
+  expect(
+    within(table).getByRole('columnheader', { name: /Database/i }),
+  ).toBeInTheDocument();
+  expect(
+    within(table).getByRole('columnheader', { name: /Schema/i }),
+  ).toBeInTheDocument();
+  expect(
+    within(table).getByRole('columnheader', { name: /Owners/i }),
+  ).toBeInTheDocument();
+  expect(
+    within(table).getByRole('columnheader', { name: /Last modified/i }),
+  ).toBeInTheDocument();
+  expect(
+    within(table).getByRole('columnheader', { name: /Actions/i }),
+  ).toBeInTheDocument();
+});
+
+test('displays dataset name in Name column', async () => {
+  const dataset = mockDatasets[0];
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+});
+
+test('displays dataset type as Physical or Virtual', async () => {
+  const physicalDataset = mockDatasets[0]; // kind: 'physical'
+  const virtualDataset = mockDatasets[1]; // kind: 'virtual'
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [physicalDataset, virtualDataset], count: 2 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(physicalDataset.table_name)).toBeInTheDocument();
+  });
+
+  expect(screen.getByText(virtualDataset.table_name)).toBeInTheDocument();
+});
+
+test('displays database name in Database column', async () => {
+  const dataset = mockDatasets[0];
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(dataset.database.database_name),
+    ).toBeInTheDocument();
+  });
+});
+
+test('displays schema name in Schema column', async () => {
+  const dataset = mockDatasets[0];
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.schema)).toBeInTheDocument();
+  });
+});
+
+test('displays last modified date in humanized format', async () => {
+  const dataset = mockDatasets[0];
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(dataset.changed_on_delta_humanized),
+    ).toBeInTheDocument();
+  });
+});
+
+test('sorting by Name column updates API call with sort parameter', async () 
=> {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const nameHeader = within(table).getByRole('columnheader', {
+    name: /Name/i,
+  });
+
+  // Record initial calls
+  const initialCalls = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+
+  // Click Name header to sort
+  await userEvent.click(nameHeader);
+
+  // Wait for new API call
+  await waitFor(() => {
+    const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+    expect(calls.length).toBeGreaterThan(initialCalls);
+  });
+
+  // Verify latest call includes sort parameter
+  const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+  const latestCall = calls[calls.length - 1];
+  const url = latestCall[0] as string;
+
+  // URL should contain order_column for sorting
+  expect(url).toMatch(/order_column|sort/);
+});
+
+test('sorting by Database column updates sort parameter', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const databaseHeader = within(table).getByRole('columnheader', {
+    name: /Database/i,
+  });
+
+  const initialCalls = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+
+  await userEvent.click(databaseHeader);
+
+  await waitFor(() => {
+    const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+    expect(calls.length).toBeGreaterThan(initialCalls);
+  });
+
+  const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+  const url = calls[calls.length - 1][0] as string;
+  expect(url).toMatch(/order_column|sort/);
+});
+
+test('sorting by Last modified column updates sort parameter', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const modifiedHeader = within(table).getByRole('columnheader', {
+    name: /Last modified/i,
+  });
+
+  const initialCalls = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+
+  await userEvent.click(modifiedHeader);
+
+  await waitFor(() => {
+    const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+    expect(calls.length).toBeGreaterThan(initialCalls);
+  });
+
+  const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+  const url = calls[calls.length - 1][0] as string;
+  expect(url).toMatch(/order_column|sort/);
+});
+
+test('export button triggers handleResourceExport with dataset ID', async () 
=> {
+  const dataset = mockDatasets[0];
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  // Find export button in actions column (fail-fast if not found)
+  const table = screen.getByTestId('listview-table');
+  const exportButton = await within(table).findByTestId('upload');
+
+  await userEvent.click(exportButton);
+
+  await waitFor(() => {
+    expect(mockHandleResourceExport).toHaveBeenCalledWith(
+      'dataset',
+      [dataset.id],
+      expect.any(Function),
+    );
+  });
+});
+
+test('delete button opens modal with dataset details', async () => {
+  const dataset = mockDatasets[0];
+
+  setupDeleteMocks(dataset.id);
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const deleteButton = await within(table).findByTestId('delete');
+
+  await userEvent.click(deleteButton);
+
+  // Verify delete modal appears
+  const modal = await screen.findByRole('dialog');
+  expect(modal).toBeInTheDocument();
+});
+
+test('duplicate button visible only for virtual datasets', async () => {
+  const physicalDataset = mockDatasets[0]; // kind: 'physical'
+  const virtualDataset = mockDatasets[1]; // kind: 'virtual'
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [physicalDataset, virtualDataset], count: 2 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(physicalDataset.table_name)).toBeInTheDocument();
+  });
+
+  // Find both dataset rows
+  const physicalRow = screen
+    .getByText(physicalDataset.table_name)
+    .closest('tr');
+  const virtualRow = screen.getByText(virtualDataset.table_name).closest('tr');
+
+  expect(physicalRow).toBeInTheDocument();
+  expect(virtualRow).toBeInTheDocument();
+
+  // Check physical dataset row - should NOT have duplicate button
+  const physicalDuplicateButton = within(physicalRow!).queryByTestId('copy');
+  expect(physicalDuplicateButton).not.toBeInTheDocument();
+
+  // Check virtual dataset row - should have duplicate button (copy icon)
+  const virtualDuplicateButton = within(virtualRow!).getByTestId('copy');
+  expect(virtualDuplicateButton).toBeInTheDocument();
+
+  // Verify the duplicate button is visible and clickable for virtual datasets
+  expect(virtualDuplicateButton).toBeVisible();
+});
+
+test('bulk select enables checkboxes for all rows', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Verify no checkboxes before bulk select
+  expect(screen.queryAllByRole('checkbox')).toHaveLength(0);
+
+  const bulkSelectButton = screen.getByRole('button', { name: /bulk select/i 
});
+  await userEvent.click(bulkSelectButton);
+
+  // Checkboxes should appear
+  await waitFor(() => {
+    const checkboxes = screen.getAllByRole('checkbox');
+    expect(checkboxes.length).toBeGreaterThan(0);
+  });
+
+  // Note: Bulk action buttons (Export, Delete) only appear after selecting 
items
+  // This test only verifies checkboxes appear - button visibility tested in 
other tests
+});
+
+test('selecting all datasets shows correct count in toolbar', async () => {
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: mockDatasets, count: mockDatasets.length },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Enter bulk select mode
+  const bulkSelectButton = screen.getByRole('button', { name: /bulk select/i 
});
+  await userEvent.click(bulkSelectButton);
+
+  await waitFor(() => {
+    const checkboxes = screen.getAllByRole('checkbox');
+    expect(checkboxes.length).toBeGreaterThan(0);
+  });
+
+  // Select all checkbox using semantic selector
+  // Note: antd renders multiple checkboxes with same aria-label, use first 
one (table header)
+  const selectAllCheckboxes = screen.getAllByLabelText('Select all');
+  await userEvent.click(selectAllCheckboxes[0]);
+
+  // Should show selected count in toolbar (use data-test for reliability)
+  await waitFor(() => {
+    expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+      `${mockDatasets.length} Selected`,
+    );
+  });
+
+  // Verify bulk action buttons are enabled when items are selected
+  const exportButton = screen.getByRole('button', { name: /export/i });
+  const deleteButton = screen.getByRole('button', { name: 'Delete' });
+  expect(exportButton).toBeEnabled();
+  expect(deleteButton).toBeEnabled();
+});
+
+test('bulk export triggers export with selected IDs', async () => {
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [mockDatasets[0]], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Enter bulk select mode
+  const bulkSelectButton = screen.getByRole('button', { name: /bulk select/i 
});
+  await userEvent.click(bulkSelectButton);
+
+  // Select checkbox
+  await waitFor(() => {
+    const checkboxes = screen.getAllByRole('checkbox');
+    expect(checkboxes.length).toBeGreaterThan(0);
+  });
+
+  const checkboxes = screen.getAllByRole('checkbox');
+  expect(checkboxes.length).toBeGreaterThan(1);
+
+  // Click first data row checkbox (index 0 might be select-all)
+  await userEvent.click(checkboxes[1]);
+
+  // Find and click bulk export button (fail-fast if not found)
+  const exportButton = await screen.findByRole('button', { name: /export/i });
+  await userEvent.click(exportButton);
+
+  await waitFor(() => {
+    expect(mockHandleResourceExport).toHaveBeenCalled();
+  });
+});
+
+test('bulk delete opens confirmation modal', async () => {
+  setupBulkDeleteMocks();
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [mockDatasets[0]], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Enter bulk select mode
+  const bulkSelectButton = screen.getByRole('button', { name: /bulk select/i 
});
+  await userEvent.click(bulkSelectButton);
+
+  // Select checkbox
+  await waitFor(() => {
+    const checkboxes = screen.getAllByRole('checkbox');
+    expect(checkboxes.length).toBeGreaterThan(0);
+  });
+
+  const checkboxes = screen.getAllByRole('checkbox');
+  expect(checkboxes.length).toBeGreaterThan(1);
+
+  await userEvent.click(checkboxes[1]);
+
+  // Find and click bulk delete button (use accessible name for specificity)
+  const deleteButton = await screen.findByRole('button', { name: 'Delete' });
+  await userEvent.click(deleteButton);
+
+  // Confirmation modal should appear
+  const modal = await screen.findByRole('dialog');
+  expect(modal).toBeInTheDocument();
+});
+
+test('exit bulk select via close button returns to normal view', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Enter bulk select mode
+  const bulkSelectButton = screen.getByRole('button', { name: /bulk select/i 
});
+  await userEvent.click(bulkSelectButton);
+
+  await waitFor(() => {
+    const checkboxes = screen.getAllByRole('checkbox');
+    expect(checkboxes.length).toBeGreaterThan(0);
+  });
+
+  // Note: Not verifying export/delete buttons here as they only appear after 
selection
+  // This test focuses on the close button functionality
+
+  // Find close button within the bulk select container using Ant Design's 
class
+  // Scoping to container prevents selecting close buttons from other 
components
+  const bulkSelectControls = screen.getByTestId('bulk-select-controls');
+  const closeButton = bulkSelectControls.querySelector(
+    '.ant-alert-close-icon',
+  ) as HTMLElement;
+  await userEvent.click(closeButton);
+
+  // Checkboxes should disappear
+  await waitFor(() => {
+    const checkboxes = screen.queryAllByRole('checkbox');
+    expect(checkboxes.length).toBe(0);
+  });
+
+  // Bulk action toolbar should be hidden, normal toolbar should return
+  await waitFor(() => {
+    expect(
+      screen.queryByTestId('bulk-select-controls'),
+    ).not.toBeInTheDocument();
+    // Bulk select button should be back
+    expect(
+      screen.getByRole('button', { name: /bulk select/i }),
+    ).toBeInTheDocument();
+  });
+});
+
+test('certified badge appears for certified datasets', async () => {
+  const certifiedDataset = {
+    ...mockDatasets[1],
+    extra: JSON.stringify({
+      certification: {
+        certified_by: 'Data Team',
+        details: 'Approved for production',
+      },
+    }),
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [certifiedDataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(certifiedDataset.table_name)).toBeInTheDocument();
+  });
+
+  // Find the dataset row
+  const row = screen.getByText(certifiedDataset.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Verify certified badge icon is present in the row
+  const certBadge = await within(row!).findByRole('img', {
+    name: /certified/i,
+  });
+  expect(certBadge).toBeInTheDocument();
+});
+
+test('warning icon appears for datasets with warnings', async () => {
+  const datasetWithWarning = {
+    ...mockDatasets[2],
+    extra: JSON.stringify({
+      warning_markdown: 'Contains PII',
+    }),
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [datasetWithWarning], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    
expect(screen.getByText(datasetWithWarning.table_name)).toBeInTheDocument();
+  });
+
+  // Find the dataset row
+  const row = screen.getByText(datasetWithWarning.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Verify warning icon is present in the row
+  const warningIcon = await within(row!).findByRole('img', {
+    name: /warning/i,
+  });
+  expect(warningIcon).toBeInTheDocument();
+});
+
+test('info tooltip appears for datasets with descriptions', async () => {
+  const datasetWithDescription = {
+    ...mockDatasets[0],
+    description: 'Sales data from Q4 2024',
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [datasetWithDescription], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(datasetWithDescription.table_name),
+    ).toBeInTheDocument();
+  });
+
+  // Find the dataset row
+  const row = 
screen.getByText(datasetWithDescription.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Verify info tooltip icon is present in the row
+  const infoIcon = await within(row!).findByRole('img', { name: /info/i });
+  expect(infoIcon).toBeInTheDocument();
+});
+
+test('dataset name links to Explore page', async () => {
+  const dataset = mockDatasets[0];
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  // Find the dataset row and scope the link query to it
+  const row = screen.getByText(dataset.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Dataset name should be a link to Explore within the row
+  const link = within(row!).getByTestId('internal-link');
+  expect(link).toHaveAttribute('href', dataset.explore_url);
+});
+
+test('physical dataset shows delete, export, and edit actions (no duplicate)', 
async () => {
+  const physicalDataset = mockDatasets[0]; // kind: 'physical'
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [physicalDataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(physicalDataset.table_name)).toBeInTheDocument();
+  });
+
+  const row = screen.getByText(physicalDataset.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Physical datasets should have: delete, export, edit
+  const deleteButton = within(row!).getByTestId('delete');
+  const exportButton = within(row!).getByTestId('upload');
+  const editButton = within(row!).getByTestId('edit');
+
+  expect(deleteButton).toBeInTheDocument();
+  expect(exportButton).toBeInTheDocument();
+  expect(editButton).toBeInTheDocument();
+
+  // Should NOT have duplicate button
+  const duplicateButton = within(row!).queryByTestId('copy');
+  expect(duplicateButton).not.toBeInTheDocument();
+});
+
+test('virtual dataset shows delete, export, edit, and duplicate actions', 
async () => {
+  const virtualDataset = mockDatasets[1]; // kind: 'virtual'
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [virtualDataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(virtualDataset.table_name)).toBeInTheDocument();
+  });
+
+  const row = screen.getByText(virtualDataset.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Virtual datasets should have: delete, export, edit, duplicate
+  const deleteButton = within(row!).getByTestId('delete');
+  const exportButton = within(row!).getByTestId('upload');
+  const editButton = within(row!).getByTestId('edit');
+  const duplicateButton = within(row!).getByTestId('copy');
+
+  expect(deleteButton).toBeInTheDocument();
+  expect(exportButton).toBeInTheDocument();
+  expect(editButton).toBeInTheDocument();
+  expect(duplicateButton).toBeInTheDocument();
+});
+
+test('edit action is enabled for dataset owner', async () => {
+  const dataset = {
+    ...mockDatasets[0],
+    owners: [{ id: mockAdminUser.userId, username: 'admin' }],
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  const row = screen.getByText(dataset.table_name).closest('tr');
+  const editIcon = within(row!).getByTestId('edit');
+  const editButton = editIcon.closest('.action-button, .disabled');
+
+  // Should have action-button class (not disabled)
+  expect(editButton).toHaveClass('action-button');
+  expect(editButton).not.toHaveClass('disabled');
+});
+
+test('edit action is disabled for non-owner', async () => {
+  const dataset = {
+    ...mockDatasets[0],
+    owners: [{ id: 999, username: 'other_user' }], // Different user
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  // Use a non-admin user to test ownership check
+  const regularUser = {
+    ...mockAdminUser,
+    roles: { Admin: [['can_read', 'Dataset']] },
+  };
+
+  renderDatasetList(regularUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  const row = screen.getByText(dataset.table_name).closest('tr');
+  const editIcon = within(row!).getByTestId('edit');
+  const editButton = editIcon.closest('.action-button, .disabled');
+
+  // Should have disabled class (disabled buttons still have 'action-button' 
class)
+  expect(editButton).toHaveClass('disabled');
+  expect(editButton).toHaveClass('action-button');
+});
+
+test('all action buttons are clickable and enabled for admin user', async () 
=> {
+  const virtualDataset = {
+    ...mockDatasets[1],
+    owners: [{ id: mockAdminUser.userId, username: 'admin' }],
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [virtualDataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(virtualDataset.table_name)).toBeInTheDocument();
+  });
+
+  const row = screen.getByText(virtualDataset.table_name).closest('tr');
+
+  // Get icons and their parent button elements
+  const deleteIcon = within(row!).getByTestId('delete');
+  const exportIcon = within(row!).getByTestId('upload');
+  const editIcon = within(row!).getByTestId('edit');
+  const duplicateIcon = within(row!).getByTestId('copy');
+
+  const deleteButton = deleteIcon.closest('.action-button, .disabled');
+  const exportButton = exportIcon.closest('.action-button, .disabled');
+  const editButton = editIcon.closest('.action-button, .disabled');
+  const duplicateButton = duplicateIcon.closest('.action-button, .disabled');
+
+  // All should have action-button class (enabled)
+  expect(deleteButton).toHaveClass('action-button');
+  expect(exportButton).toHaveClass('action-button');
+  expect(editButton).toHaveClass('action-button');
+  expect(duplicateButton).toHaveClass('action-button');
+
+  // None should be disabled
+  expect(deleteButton).not.toHaveClass('disabled');
+  expect(exportButton).not.toHaveClass('disabled');
+  expect(editButton).not.toHaveClass('disabled');
+  expect(duplicateButton).not.toHaveClass('disabled');
+});
+
+test('delete action shows error toast on 403 forbidden', async () => {
+  const dataset = mockDatasets[0];
+
+  setupErrorTestScenario({
+    dataset,
+    method: 'get',
+    endpoint: '/related_objects',
+    errorStatus: 403,
+    errorMessage: 'Failed to fetch related objects',
+  });
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const deleteButton = await within(table).findByTestId('delete');
+
+  await userEvent.click(deleteButton);
+
+  // Wait for error toast with combined assertion
+  await waitFor(() =>
+    expect(mockAddDangerToast).toHaveBeenCalledWith(
+      expect.stringMatching(/error occurred while fetching dataset/i),
+    ),
+  );
+
+  // Verify modal did NOT open (error prevented it)
+  const modal = screen.queryByRole('dialog');
+  expect(modal).not.toBeInTheDocument();
+
+  // Verify dataset still in list (not removed)
+  expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+});
+
+test('delete action shows error toast on 500 internal server error', async () 
=> {
+  const dataset = mockDatasets[0];
+
+  setupErrorTestScenario({
+    dataset,
+    method: 'get',
+    endpoint: '/related_objects',
+    errorStatus: 500,
+    errorMessage: 'Internal Server Error',
+  });
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const deleteButton = await within(table).findByTestId('delete');
+
+  await userEvent.click(deleteButton);
+
+  // Wait for error toast with combined assertion
+  await waitFor(() =>
+    expect(mockAddDangerToast).toHaveBeenCalledWith(
+      expect.stringMatching(/error occurred while fetching dataset/i),
+    ),
+  );
+
+  // Verify modal did NOT open
+  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+
+  // Verify table state unchanged
+  expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+});
+
+test('duplicate action shows error toast on 403 forbidden', async () => {
+  const virtualDataset = {
+    ...mockDatasets[1],
+    owners: [
+      {
+        first_name: mockAdminUser.firstName,
+        last_name: mockAdminUser.lastName,
+        id: mockAdminUser.userId as number,
+      },
+    ],
+  };
+
+  setupErrorTestScenario({
+    dataset: virtualDataset,
+    method: 'post',
+    endpoint: '/duplicate',
+    errorStatus: 403,
+    errorMessage: 'Failed to duplicate dataset',
+  });
+
+  await waitFor(() => {
+    expect(screen.getByText(virtualDataset.table_name)).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const duplicateButton = await within(table).findByTestId('copy');
+
+  await userEvent.click(duplicateButton);
+
+  // Wait for duplicate modal to appear
+  const modal = await screen.findByRole('dialog');
+  expect(modal).toBeInTheDocument();
+
+  // Enter new dataset name
+  const input = within(modal).getByRole('textbox');
+  await userEvent.clear(input);
+  await userEvent.type(input, 'Copy of Analytics Query');
+
+  // Submit duplicate
+  const submitButton = within(modal).getByRole('button', {
+    name: /duplicate/i,
+  });
+  await userEvent.click(submitButton);
+
+  // Wait for modal to close (error handler closes it)
+  // antd modal close animation can be slow, increase timeout
+  await waitFor(
+    () => {
+      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+    },
+    { timeout: 10000 },
+  );
+
+  // Wait for error toast
+  await waitFor(() =>
+    expect(mockAddDangerToast).toHaveBeenCalledWith(
+      expect.stringMatching(/issue duplicating.*selected datasets/i),
+    ),
+  );
+
+  // Verify table state unchanged (no new dataset added)
+  const allDatasetRows = screen.getAllByRole('row');
+  // Header + 1 dataset row
+  expect(allDatasetRows.length).toBe(2);
+});
+
+test('duplicate action shows error toast on 500 internal server error', async 
() => {
+  const virtualDataset = {
+    ...mockDatasets[1],
+    owners: [
+      {
+        first_name: mockAdminUser.firstName,
+        last_name: mockAdminUser.lastName,
+        id: mockAdminUser.userId as number,
+      },
+    ],
+  };
+
+  setupErrorTestScenario({
+    dataset: virtualDataset,
+    method: 'post',
+    endpoint: '/duplicate',
+    errorStatus: 500,
+    errorMessage: 'Internal Server Error',
+  });
+
+  await waitFor(() => {
+    expect(screen.getByText(virtualDataset.table_name)).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const duplicateButton = await within(table).findByTestId('copy');
+
+  await userEvent.click(duplicateButton);
+
+  // Wait for duplicate modal
+  const modal = await screen.findByRole('dialog');
+
+  // Enter new dataset name
+  const input = within(modal).getByRole('textbox');
+  await userEvent.clear(input);
+  await userEvent.type(input, 'Copy of Analytics Query');
+
+  // Submit
+  const submitButton = within(modal).getByRole('button', {
+    name: /duplicate/i,
+  });
+  await userEvent.click(submitButton);
+
+  // Wait for modal to close (error handler closes it)
+  // antd modal close animation can be slow, increase timeout
+  await waitFor(
+    () => {
+      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+    },
+    { timeout: 10000 },
+  );
+
+  // Wait for error toast
+  await waitFor(() =>
+    expect(mockAddDangerToast).toHaveBeenCalledWith(
+      expect.stringMatching(/issue duplicating.*selected datasets/i),
+    ),
+  );
+
+  // Verify table state unchanged
+  expect(screen.getByText(virtualDataset.table_name)).toBeInTheDocument();
+});
+
+// Component "+1" Tests - State persistence through operations
+
+test('sort order persists after deleting a dataset', async () => {
+  const datasetToDelete = mockDatasets[0];
+  setupDeleteMocks(datasetToDelete.id);
+
+  renderDatasetList(mockAdminUser, {
+    addSuccessToast: mockAddSuccessToast,
+    addDangerToast: mockAddDangerToast,
+  });
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const nameHeader = within(table).getByRole('columnheader', {
+    name: /Name/i,
+  });
+
+  // Record initial API calls count
+  const initialCalls = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+
+  // Click Name header to sort
+  await userEvent.click(nameHeader);
+
+  // Wait for new API call with sort parameter
+  await waitFor(() => {
+    const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+    expect(calls.length).toBeGreaterThan(initialCalls);
+  });
+
+  // Record the sort parameter from the API call after sorting
+  const callsAfterSort = fetchMock.calls(API_ENDPOINTS.DATASETS);
+  const sortedUrl = callsAfterSort[callsAfterSort.length - 1][0] as string;
+  expect(sortedUrl).toMatch(/order_column|sort/);
+
+  // Delete a dataset - get delete button from first row only
+  const firstRow = screen.getAllByRole('row')[1];
+  const deleteButton = within(firstRow).getByTestId('delete');
+  await userEvent.click(deleteButton);
+
+  // Confirm delete in modal - type DELETE to enable button
+  const modal = await screen.findByRole('dialog');
+  await within(modal).findByText(datasetToDelete.table_name);
+
+  // Enable the danger button by typing DELETE
+  const confirmInput = within(modal).getByTestId('delete-modal-input');
+  await userEvent.clear(confirmInput);
+  await userEvent.type(confirmInput, 'DELETE');
+
+  // Record call count before delete to track refetch
+  const callsBeforeDelete = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+
+  const confirmButton = within(modal)
+    .getAllByRole('button', { name: /^delete$/i })
+    .pop();
+  await userEvent.click(confirmButton!);
+
+  // Confirm the delete request fired
+  await waitFor(() => {
+    expect(mockAddSuccessToast).toHaveBeenCalled();
+  });
+
+  // Wait for modal to close completely
+  await waitFor(() => {
+    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+  });
+
+  // Wait for list refetch to complete (prevents async cleanup error)
+  await waitFor(() => {
+    const currentCalls = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+    expect(currentCalls).toBeGreaterThan(callsBeforeDelete);
+  });
+
+  // Now re-query the header and assert the sort indicators still exist
+  await waitFor(() => {
+    const carets = within(nameHeader.closest('th')!).getAllByLabelText(
+      /caret/i,
+    );
+    expect(carets.length).toBeGreaterThan(0);
+  });
+});
+
+// Note: "deleting last item on page 2 fetches page 1" is a hook-level 
pagination
+// concern (useListViewResource handles page reset logic). This is covered by
+// integration tests where we can verify the full pagination cycle.
+
+test('bulk delete refreshes list with updated count', async () => {
+  setupBulkDeleteMocks();
+
+  renderDatasetList(mockAdminUser, {
+    addSuccessToast: mockAddSuccessToast,
+    addDangerToast: mockAddDangerToast,
+  });
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Enter bulk select mode
+  const bulkSelectButton = screen.getByRole('button', {
+    name: /bulk select/i,
+  });
+  await userEvent.click(bulkSelectButton);
+
+  await waitFor(() => {
+    const checkboxes = screen.getAllByRole('checkbox');
+    expect(checkboxes.length).toBeGreaterThan(0);
+  });
+
+  // Select first 3 items (re-query checkboxes after each click to handle DOM 
updates)
+  let checkboxes = screen.getAllByRole('checkbox');
+  await userEvent.click(checkboxes[1]);
+
+  checkboxes = screen.getAllByRole('checkbox');
+  await userEvent.click(checkboxes[2]);
+
+  checkboxes = screen.getAllByRole('checkbox');
+  await userEvent.click(checkboxes[3]);
+
+  // Wait for selections to register
+  await waitFor(() => {
+    const selectionText = screen.getByText(/selected/i);
+    expect(selectionText).toBeInTheDocument();
+    expect(selectionText).toHaveTextContent('3');
+  });
+
+  // Verify bulk actions UI appears and click the bulk delete button
+  // Multiple bulk actions share the same test ID, so filter by text content
+  const bulkActionButtons = await screen.findAllByTestId('bulk-select-action');
+  const bulkDeleteButton = bulkActionButtons.find(btn =>
+    btn.textContent?.includes('Delete'),
+  );
+  expect(bulkDeleteButton).toBeTruthy();
+
+  await userEvent.click(bulkDeleteButton!);
+
+  // Confirm in modal - type DELETE to enable button
+  const modal = await screen.findByRole('dialog');
+
+  // Enable the danger button by typing DELETE
+  const confirmInput = within(modal).getByTestId('delete-modal-input');
+  await userEvent.clear(confirmInput);
+  await userEvent.type(confirmInput, 'DELETE');
+
+  const confirmButton = within(modal)
+    .getAllByRole('button', { name: /^delete$/i })
+    .pop();
+  await userEvent.click(confirmButton!);
+
+  // Wait for modal to close first (defensive wait for CI stability)
+  await waitFor(() => {
+    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+  });
+
+  // Wait for success toast
+  await waitFor(() => {
+    expect(mockAddSuccessToast).toHaveBeenCalledWith(
+      expect.stringContaining('deleted'),
+    );
+  });
+
+  // Verify danger toast was not called
+  expect(mockAddDangerToast).not.toHaveBeenCalled();
+}, 30000); // 30 second timeout for slow bulk delete test
+
+test('bulk selection clears when filter changes', async () => {
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: mockDatasets, count: mockDatasets.length },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Enter bulk select mode
+  const bulkSelectButton = screen.getByRole('button', {
+    name: /bulk select/i,
+  });
+  await userEvent.click(bulkSelectButton);
+
+  await waitFor(() => {
+    const checkboxes = screen.getAllByRole('checkbox');
+    expect(checkboxes.length).toBeGreaterThan(0);
+  });
+
+  // Select first 2 items
+  const checkboxes = screen.getAllByRole('checkbox');
+  await userEvent.click(checkboxes[1]);
+  await userEvent.click(checkboxes[2]);
+
+  // Wait for selections to register - assert on "selected" text which is what 
users see
+  await screen.findByText(/selected/i);
+
+  // Record API call count before filter
+  const beforeFilterCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+
+  // Apply a filter using selectOption helper
+  await selectOption('Virtual', 'Type');
+
+  // Wait for filter API call to complete
+  await waitFor(() => {
+    const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+    expect(calls.length).toBeGreaterThan(beforeFilterCallCount);
+  });
+
+  // Verify filter was applied by decoding URL payload
+  const urlAfterFilter = fetchMock
+    .calls(API_ENDPOINTS.DATASETS)
+    .at(-1)?.[0] as string;
+  const risonAfterFilter = urlAfterFilter.split('?q=')[1];
+  const decodedAfterFilter = rison.decode(
+    decodeURIComponent(risonAfterFilter!),
+  ) as Record<string, any>;
+  expect(decodedAfterFilter.filters).toEqual(
+    expect.arrayContaining([
+      expect.objectContaining({ col: 'sql', value: false }),
+    ]),
+  );
+
+  // Verify selection was cleared - count should show "0 Selected"
+  await waitFor(() => {
+    expect(screen.getByText(/0 selected/i)).toBeInTheDocument();
+  });
+}, 30000); // 30 second timeout for slow CI environment
+
+test('type filter persists after duplicating a dataset', async () => {
+  const datasetToDuplicate = mockDatasets.find(d => d.kind === 'virtual')!;
+
+  setupDuplicateMocks();
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Apply Type filter using selectOption helper
+  // Check if filter is already applied (from previous test state)
+  const typeFilterCombobox = screen.queryByRole('combobox', { name: /^Type:/ 
});
+  if (!typeFilterCombobox) {
+    // Filter not applied yet, apply it
+    await selectOption('Virtual', 'Type');
+  }
+
+  // Wait a moment for any pending filter operations to complete
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Verify filter is present by checking the latest API call
+  const urlAfterFilter = fetchMock
+    .calls(API_ENDPOINTS.DATASETS)
+    .at(-1)?.[0] as string;
+  const risonAfterFilter = urlAfterFilter.split('?q=')[1];
+  const decodedAfterFilter = rison.decode(
+    decodeURIComponent(risonAfterFilter!),
+  ) as Record<string, any>;
+  expect(decodedAfterFilter.filters).toEqual(
+    expect.arrayContaining([
+      expect.objectContaining({ col: 'sql', value: false }),
+    ]),
+  );
+
+  // Capture datasets API call count BEFORE any duplicate operations
+  const datasetsCallCountBeforeDuplicate = fetchMock.calls(
+    API_ENDPOINTS.DATASETS,
+  ).length;
+
+  // Now duplicate the dataset
+  const row = screen.getByText(datasetToDuplicate.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  const duplicateIcon = await within(row!).findByTestId('copy');
+  const duplicateButton = duplicateIcon.closest(
+    '[role="button"]',
+  ) as HTMLElement | null;
+  expect(duplicateButton).toBeTruthy();
+
+  await userEvent.click(duplicateButton!);
+
+  const modal = await screen.findByRole('dialog');
+  const modalInput = within(modal).getByRole('textbox');
+  await userEvent.clear(modalInput);
+  await userEvent.type(modalInput, 'Copy of Dataset');
+
+  const confirmButton = within(modal).getByRole('button', {
+    name: /duplicate/i,
+  });
+  await userEvent.click(confirmButton);
+
+  // Wait for duplicate API call to be made
+  await waitFor(() => {
+    const duplicateCalls = fetchMock.calls(API_ENDPOINTS.DATASET_DUPLICATE);
+    expect(duplicateCalls.length).toBeGreaterThan(0);
+  });
+
+  // Wait for datasets refetch to occur (proves duplicate triggered a refresh)
+  await waitFor(() => {
+    const datasetsCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+    
expect(datasetsCallCount).toBeGreaterThan(datasetsCallCountBeforeDuplicate);
+  });
+
+  // Verify Type filter persisted in the NEW datasets API call after 
duplication
+  const urlAfterDuplicate = fetchMock
+    .calls(API_ENDPOINTS.DATASETS)
+    .at(-1)?.[0] as string;
+  const risonAfterDuplicate = urlAfterDuplicate.split('?q=')[1];
+  const decodedAfterDuplicate = rison.decode(
+    decodeURIComponent(risonAfterDuplicate!),
+  ) as Record<string, any>;
+  expect(decodedAfterDuplicate.filters).toEqual(
+    expect.arrayContaining([
+      expect.objectContaining({ col: 'sql', value: false }),
+    ]),
+  );
+});
+
+test('type filter API call includes correct filter parameter', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Apply Type filter using selectOption helper
+  // Check if filter is already applied (from previous test state)
+  const typeFilterCombobox = screen.queryByRole('combobox', { name: /^Type:/ 
});
+  if (!typeFilterCombobox) {
+    // Filter not applied yet, apply it
+    await selectOption('Virtual', 'Type');
+  }
+
+  // Wait a moment for any pending filter operations to complete
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  // Verify the latest API call includes the Type filter
+  const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+  const latestCall = calls[calls.length - 1];
+  const url = latestCall[0] as string;
+
+  // URL should contain filters parameter
+  expect(url).toContain('filters');
+  const risonPayload = url.split('?q=')[1];
+  expect(risonPayload).toBeTruthy();
+  const decoded = rison.decode(decodeURIComponent(risonPayload!)) as Record<
+    string,
+    unknown
+  >;
+  const filters = Array.isArray(decoded?.filters) ? decoded.filters : [];
+
+  // Type filter should be present (sql=false for Virtual datasets)
+  const hasTypeFilter = filters.some(
+    (filter: Record<string, unknown>) =>
+      filter?.col === 'sql' && filter?.value === false,
+  );
+
+  expect(hasTypeFilter).toBe(true);
+});
diff --git 
a/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx 
b/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx
new file mode 100644
index 0000000000..14a73e264a
--- /dev/null
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx
@@ -0,0 +1,394 @@
+/**
+ * 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 { cleanup, screen, waitFor, within } from '@testing-library/react';
+import fetchMock from 'fetch-mock';
+import {
+  setupMocks,
+  setupApiPermissions,
+  renderDatasetList,
+  mockAdminUser,
+  mockReadOnlyUser,
+  mockWriteUser,
+  mockExportOnlyUser,
+  mockDatasets,
+  API_ENDPOINTS,
+} from './DatasetList.testHelpers';
+
+beforeEach(() => {
+  setupMocks();
+  jest.clearAllMocks();
+});
+
+afterEach(() => {
+  cleanup();
+  fetchMock.reset();
+});
+
+test('admin users see all UI elements', async () => {
+  // Setup API with full admin permissions
+  setupApiPermissions(['can_read', 'can_write', 'can_export', 
'can_duplicate']);
+
+  renderDatasetList(mockAdminUser);
+
+  expect(await screen.findByText('Datasets')).toBeInTheDocument();
+
+  // Admin should see create button
+  expect(screen.getByRole('button', { name: /dataset/i })).toBeInTheDocument();
+
+  // Admin should see import button
+  // Note: Using testId - import button lacks accessible text content
+  // TODO: Add aria-label or text to import button
+  expect(screen.getByTestId('import-button')).toBeInTheDocument();
+
+  // Admin should see bulk select button
+  expect(
+    screen.getByRole('button', { name: /bulk select/i }),
+  ).toBeInTheDocument();
+
+  // Admin should see actions column
+  await waitFor(() => {
+    const table = screen.getByTestId('listview-table');
+    expect(
+      within(table).getByRole('columnheader', { name: /Actions/i }),
+    ).toBeInTheDocument();
+  });
+});
+
+test('read-only users cannot see Actions column', async () => {
+  // Setup API with read-only permissions
+  setupApiPermissions(['can_read']);
+
+  renderDatasetList(mockReadOnlyUser);
+
+  await waitFor(() => {
+    expect(screen.getByText('Datasets')).toBeInTheDocument();
+  });
+
+  await waitFor(() => {
+    const table = screen.getByTestId('listview-table');
+    // Actions column should not be present
+    expect(within(table).queryByText(/Actions/i)).not.toBeInTheDocument();
+  });
+});
+
+test('read-only users cannot see bulk select button', async () => {
+  // Setup API with read-only permissions
+  setupApiPermissions(['can_read']);
+
+  renderDatasetList(mockReadOnlyUser);
+
+  await waitFor(() => {
+    expect(screen.getByText('Datasets')).toBeInTheDocument();
+  });
+
+  // Bulk select should not be visible
+  expect(
+    screen.queryByRole('button', { name: /bulk select/i }),
+  ).not.toBeInTheDocument();
+});
+
+test('read-only users cannot see Create/Import buttons', async () => {
+  // Setup API with read-only permissions
+  setupApiPermissions(['can_read']);
+
+  renderDatasetList(mockReadOnlyUser);
+
+  await waitFor(() => {
+    expect(screen.getByText('Datasets')).toBeInTheDocument();
+  });
+
+  // Create button should not be visible
+  expect(
+    screen.queryByRole('button', { name: /dataset/i }),
+  ).not.toBeInTheDocument();
+
+  // Import button should not be visible
+  // Note: Using testId - import button lacks accessible text content
+  // TODO: Add aria-label or text to import button
+  expect(screen.queryByTestId('import-button')).not.toBeInTheDocument();
+});
+
+test('write users see Actions column', async () => {
+  // Setup API with write permissions
+  setupApiPermissions(['can_read', 'can_write', 'can_export']);
+
+  renderDatasetList(mockWriteUser);
+
+  await waitFor(() => {
+    expect(screen.getByText('Datasets')).toBeInTheDocument();
+  });
+
+  await waitFor(() => {
+    const table = screen.getByTestId('listview-table');
+    expect(
+      within(table).getByRole('columnheader', { name: /Actions/i }),
+    ).toBeInTheDocument();
+  });
+});
+
+test('write users see bulk select button', async () => {
+  // Setup API with write permissions
+  setupApiPermissions(['can_read', 'can_write', 'can_export']);
+
+  renderDatasetList(mockWriteUser);
+
+  await waitFor(() => {
+    expect(screen.getByText('Datasets')).toBeInTheDocument();
+  });
+
+  expect(
+    screen.getByRole('button', { name: /bulk select/i }),
+  ).toBeInTheDocument();
+});
+
+test('write users see Create/Import buttons', async () => {
+  // Setup API with write permissions
+  setupApiPermissions(['can_read', 'can_write', 'can_export']);
+
+  renderDatasetList(mockWriteUser);
+
+  await waitFor(() => {
+    expect(screen.getByText('Datasets')).toBeInTheDocument();
+  });
+
+  // Create button should be visible
+  expect(screen.getByRole('button', { name: /dataset/i })).toBeInTheDocument();
+
+  // Import button should be visible
+  // Note: Using testId - import button lacks accessible text content
+  // TODO: Add aria-label or text to import button
+  expect(screen.getByTestId('import-button')).toBeInTheDocument();
+});
+
+test('export-only users see bulk select (for export only)', async () => {
+  // Setup API with export-only permissions
+  setupApiPermissions(['can_read', 'can_export']);
+
+  renderDatasetList(mockExportOnlyUser);
+
+  await waitFor(() => {
+    expect(screen.getByText('Datasets')).toBeInTheDocument();
+  });
+
+  // Export users should see bulk select for export functionality
+  expect(
+    screen.getByRole('button', { name: /bulk select/i }),
+  ).toBeInTheDocument();
+});
+
+test('export-only users cannot see Create/Import buttons', async () => {
+  // Setup API with export-only permissions
+  setupApiPermissions(['can_read', 'can_export']);
+
+  renderDatasetList(mockExportOnlyUser);
+
+  await waitFor(() => {
+    expect(screen.getByText('Datasets')).toBeInTheDocument();
+  });
+
+  // Create and Import should not be visible for export-only users
+  expect(
+    screen.queryByRole('button', { name: /dataset/i }),
+  ).not.toBeInTheDocument();
+  // Note: Using testId - import button lacks accessible text content
+  // TODO: Add aria-label or text to import button
+  expect(screen.queryByTestId('import-button')).not.toBeInTheDocument();
+});
+
+test('action buttons respect user permissions', async () => {
+  // Setup API with full admin permissions
+  setupApiPermissions(['can_read', 'can_write', 'can_export', 
'can_duplicate']);
+
+  const dataset = mockDatasets[0];
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  // Admin should see action buttons in the row
+  const row = screen.getByText(dataset.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Verify specific action buttons are present
+  const deleteButton = within(row!).queryByTestId('delete');
+  const exportButton = within(row!).queryByTestId('upload');
+
+  expect(deleteButton).toBeInTheDocument();
+  expect(exportButton).toBeInTheDocument();
+});
+
+test('read-only user sees no delete or duplicate buttons in row', async () => {
+  // Setup API with read-only permissions
+  setupApiPermissions(['can_read']);
+
+  const dataset = mockDatasets[0];
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockReadOnlyUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  // Find the dataset row
+  const row = screen.getByText(dataset.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Verify no delete button in the row
+  const deleteButton = within(row!).queryByTestId('delete');
+  expect(deleteButton).not.toBeInTheDocument();
+
+  // Verify no duplicate button (Actions column should not exist)
+  const duplicateButton = within(row!).queryByTestId('copy');
+  expect(duplicateButton).not.toBeInTheDocument();
+
+  // Verify no edit button
+  const editButton = within(row!).queryByTestId('edit');
+  expect(editButton).not.toBeInTheDocument();
+});
+
+test('write user sees edit, delete, and export actions', async () => {
+  // Setup API with write permissions (includes delete)
+  // Note: can_write grants both edit and delete permissions in DatasetList
+  setupApiPermissions(['can_read', 'can_write', 'can_export']);
+
+  const dataset = {
+    ...mockDatasets[0],
+    owners: [{ id: mockWriteUser.userId, username: 'writeuser' }],
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockWriteUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  const row = screen.getByText(dataset.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Should have delete button (can_write includes delete)
+  const deleteButton = within(row!).getByTestId('delete');
+  expect(deleteButton).toBeInTheDocument();
+
+  // Should have export button
+  const exportButton = within(row!).getByTestId('upload');
+  expect(exportButton).toBeInTheDocument();
+
+  // Should have edit button (user is owner)
+  const editButton = within(row!).getByTestId('edit');
+  expect(editButton).toBeInTheDocument();
+
+  // Should NOT have duplicate button (no can_duplicate permission)
+  const duplicateButton = within(row!).queryByTestId('copy');
+  expect(duplicateButton).not.toBeInTheDocument();
+});
+
+test('export-only user has no Actions column (no write/duplicate 
permissions)', async () => {
+  // Setup API with export-only permissions
+  // Note: Export action alone doesn't render Actions column - it's in 
toolbar/bulk select
+  setupApiPermissions(['can_read', 'can_export']);
+
+  const dataset = mockDatasets[0];
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockExportOnlyUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  const row = screen.getByText(dataset.table_name).closest('tr');
+  expect(row).toBeInTheDocument();
+
+  // Actions column is hidden when user only has export permission
+  // (export is available via bulk select toolbar, not row actions)
+  const deleteButton = within(row!).queryByTestId('delete');
+  expect(deleteButton).not.toBeInTheDocument();
+
+  const editButton = within(row!).queryByTestId('edit');
+  expect(editButton).not.toBeInTheDocument();
+
+  const duplicateButton = within(row!).queryByTestId('copy');
+  expect(duplicateButton).not.toBeInTheDocument();
+
+  const exportButton = within(row!).queryByTestId('upload');
+  expect(exportButton).not.toBeInTheDocument();
+});
+
+test('user with can_duplicate sees duplicate button only for virtual 
datasets', async () => {
+  // Setup API with duplicate permission
+  setupApiPermissions(['can_read', 'can_duplicate']);
+
+  const physicalDataset = mockDatasets[0]; // kind: 'physical'
+  const virtualDataset = mockDatasets[1]; // kind: 'virtual'
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [physicalDataset, virtualDataset], count: 2 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(physicalDataset.table_name)).toBeInTheDocument();
+  });
+
+  // Check physical dataset row
+  const physicalRow = screen
+    .getByText(physicalDataset.table_name)
+    .closest('tr');
+  expect(physicalRow).toBeInTheDocument();
+
+  // Physical dataset should NOT have duplicate button
+  const physicalDuplicateButton = within(physicalRow!).queryByTestId('copy');
+  expect(physicalDuplicateButton).not.toBeInTheDocument();
+
+  // Check virtual dataset row
+  const virtualRow = screen.getByText(virtualDataset.table_name).closest('tr');
+  expect(virtualRow).toBeInTheDocument();
+
+  // Virtual dataset SHOULD have duplicate button
+  const virtualDuplicateButton = within(virtualRow!).getByTestId('copy');
+  expect(virtualDuplicateButton).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx 
b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx
new file mode 100644
index 0000000000..d1aace18c6
--- /dev/null
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx
@@ -0,0 +1,528 @@
+/**
+ * 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 { cleanup, screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import rison from 'rison';
+import fetchMock from 'fetch-mock';
+import {
+  setupMocks,
+  renderDatasetList,
+  waitForDatasetsPageReady,
+  mockAdminUser,
+  mockReadOnlyUser,
+  mockExportOnlyUser,
+  mockDatasets,
+  mockApiError403,
+  API_ENDPOINTS,
+  RisonFilter,
+} from './DatasetList.testHelpers';
+
+// eslint-disable-next-line import/no-extraneous-dependencies
+
+beforeEach(() => {
+  setupMocks();
+});
+
+afterEach(() => {
+  cleanup();
+  fetchMock.reset();
+});
+
+test('renders page with "Datasets" title', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitForDatasetsPageReady();
+});
+
+test('shows loading state during initial data fetch', () => {
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    new Promise(() => {}), // Never resolves to keep loading state
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  expect(screen.getByRole('status')).toBeInTheDocument();
+});
+
+test('maintains component structure during loading', () => {
+  fetchMock.get(API_ENDPOINTS.DATASETS, new Promise(() => {}), {
+    overwriteRoutes: true,
+  });
+
+  renderDatasetList(mockAdminUser);
+
+  expect(screen.getByText('Datasets')).toBeInTheDocument();
+  expect(screen.getByRole('status')).toBeInTheDocument();
+});
+
+test('"New Dataset" button exists (when canCreate=true)', async () => {
+  renderDatasetList(mockAdminUser);
+
+  expect(
+    await screen.findByRole('button', { name: /dataset/i }),
+  ).toBeInTheDocument();
+});
+
+test('"New Dataset" button hidden (when canCreate=false)', async () => {
+  renderDatasetList(mockReadOnlyUser);
+
+  await waitFor(() => {
+    expect(
+      screen.queryByRole('button', { name: /dataset/i }),
+    ).not.toBeInTheDocument();
+  });
+});
+
+test('"Import" button exists (when canCreate=true)', async () => {
+  renderDatasetList(mockAdminUser);
+
+  // Note: Using testId - import button lacks accessible text content
+  // TODO: Add aria-label or text to import button
+  expect(await screen.findByTestId('import-button')).toBeInTheDocument();
+});
+
+test('"Import" button opens import modal', async () => {
+  renderDatasetList(mockAdminUser);
+
+  // Note: Using testId - import button lacks accessible text content
+  // TODO: Add aria-label or text to import button
+  const importButton = await screen.findByTestId('import-button');
+  expect(importButton).toBeInTheDocument();
+
+  await userEvent.click(importButton);
+
+  // Modal should appear with title - using semantic query here
+  expect(await screen.findByRole('dialog')).toBeInTheDocument();
+  expect(screen.getByText('Import dataset')).toBeInTheDocument();
+});
+
+test('"Bulk select" button exists (when canDelete || canExport)', async () => {
+  renderDatasetList(mockAdminUser);
+
+  expect(
+    await screen.findByRole('button', { name: /bulk select/i }),
+  ).toBeInTheDocument();
+});
+
+test('"Bulk select" button exists for export-only users', async () => {
+  renderDatasetList(mockExportOnlyUser);
+
+  expect(
+    await screen.findByRole('button', { name: /bulk select/i }),
+  ).toBeInTheDocument();
+});
+
+test('"Bulk select" button hidden for read-only users', async () => {
+  renderDatasetList(mockReadOnlyUser);
+
+  await waitFor(() => {
+    expect(
+      screen.queryByRole('button', { name: /bulk select/i }),
+    ).not.toBeInTheDocument();
+  });
+});
+
+test('renders Name search filter', async () => {
+  renderDatasetList(mockAdminUser);
+
+  // Note: Using testId - search input lacks accessible label
+  // TODO: Add aria-label to search input
+  expect(
+    await screen.findByTestId('search-filter-container'),
+  ).toBeInTheDocument();
+});
+
+test('renders Type filter (Virtual/Physical dropdown)', async () => {
+  renderDatasetList(mockAdminUser);
+
+  // Filter dropdowns should be present
+  const filters = await screen.findAllByRole('combobox');
+  expect(filters.length).toBeGreaterThan(0);
+});
+
+test('handles datasets with missing fields and renders gracefully', async () 
=> {
+  const datasetWithMissingFields = {
+    id: 999,
+    table_name: 'Incomplete Dataset',
+    kind: 'physical',
+    schema: null,
+    database: {
+      id: '1',
+      database_name: 'PostgreSQL',
+    },
+    owners: [],
+    changed_by_name: 'Unknown',
+    changed_by: null,
+    changed_on_delta_humanized: 'Unknown',
+    explore_url: '/explore/?datasource=999__table',
+    extra: JSON.stringify({}),
+    sql: null,
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [datasetWithMissingFields], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText('Incomplete Dataset')).toBeInTheDocument();
+  });
+
+  // Verify empty owners renders without crashing (no FacePile)
+  const table = screen.getByRole('table');
+  expect(table).toBeInTheDocument();
+
+  // Verify the row exists even with missing data
+  const datasetRow = screen.getByText('Incomplete Dataset').closest('tr');
+  expect(datasetRow).toBeInTheDocument();
+
+  // Verify no certification badge or warning icon (extra is empty)
+  expect(
+    screen.queryByRole('img', { name: /certified/i }),
+  ).not.toBeInTheDocument();
+});
+
+test('handles empty results (shows empty state)', async () => {
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [], count: 0 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  // Datasets heading should still be present
+  expect(await screen.findByText('Datasets')).toBeInTheDocument();
+});
+
+test('makes correct initial API call on load', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+    expect(calls.length).toBeGreaterThan(0);
+  });
+});
+
+test('API call includes correct page size', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+    expect(calls.length).toBeGreaterThan(0);
+    const url = calls[0][0] as string;
+    expect(url).toContain('page_size');
+  });
+});
+
+test('typing in name filter updates input value and triggers API with decoded 
search filter', async () => {
+  renderDatasetList(mockAdminUser);
+
+  const searchContainer = await screen.findByTestId('search-filter-container');
+  const searchInput = within(searchContainer).getByRole('textbox');
+
+  // Record initial API calls
+  const initialCallCount = fetchMock.calls(API_ENDPOINTS.DATASETS).length;
+
+  // Type in search box and press Enter to trigger search
+  await userEvent.type(searchInput, 'sales{enter}');
+
+  // Verify input value updated
+  await waitFor(() => {
+    expect(searchInput).toHaveValue('sales');
+  });
+
+  // Wait for API call after Enter key press
+  await waitFor(
+    () => {
+      const calls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+      expect(calls.length).toBeGreaterThan(initialCallCount);
+
+      // Get latest API call
+      const url = calls[calls.length - 1][0] as string;
+
+      // Verify URL contains search filter
+      expect(url).toContain('filters');
+
+      // Extract and decode rison query param
+      const queryString = url.split('?q=')[1];
+      expect(queryString).toBeTruthy();
+
+      // Decode the rison payload
+      const decoded = rison.decode(decodeURIComponent(queryString)) as Record<
+        string,
+        unknown
+      >;
+
+      // Verify filter structure contains table_name search
+      expect(decoded.filters).toBeDefined();
+      expect(Array.isArray(decoded.filters)).toBe(true);
+
+      // Check for sales filter in the filters array
+      const filters = decoded.filters as RisonFilter[];
+      const hasSalesFilter = filters.some(
+        (filter: RisonFilter) =>
+          filter.col === 'table_name' &&
+          filter.opr === 'ct' &&
+          typeof filter.value === 'string' &&
+          filter.value.toLowerCase().includes('sales'),
+      );
+      expect(hasSalesFilter).toBe(true);
+    },
+    { timeout: 5000 },
+  );
+});
+
+test('toggling bulk select mode shows checkboxes', async () => {
+  renderDatasetList(mockAdminUser);
+
+  const bulkSelectButton = await screen.findByRole('button', {
+    name: /bulk select/i,
+  });
+
+  await userEvent.click(bulkSelectButton);
+
+  await waitFor(() => {
+    // When bulk select is active, checkboxes should appear
+    const checkboxes = screen.queryAllByRole('checkbox');
+    expect(checkboxes.length).toBeGreaterThan(0);
+  });
+});
+
+test('handles 500 error on initial load without crashing', async () => {
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { throws: new Error('Internal Server Error') },
+    {
+      overwriteRoutes: true,
+    },
+  );
+
+  renderDatasetList(mockAdminUser, {
+    addDangerToast: jest.fn(),
+    addSuccessToast: jest.fn(),
+  });
+
+  // Component should still render without crashing
+  await waitForDatasetsPageReady();
+});
+
+test('handles 403 error on _info endpoint and disables create actions', async 
() => {
+  const addDangerToast = jest.fn();
+
+  fetchMock.get(API_ENDPOINTS.DATASETS_INFO, mockApiError403, {
+    overwriteRoutes: true,
+  });
+
+  renderDatasetList(mockAdminUser, {
+    addDangerToast,
+    addSuccessToast: jest.fn(),
+  });
+
+  await waitForDatasetsPageReady();
+
+  // Verify bulk actions are disabled/hidden when permissions fail
+  await waitFor(() => {
+    const bulkSelectButton = screen.queryByRole('button', {
+      name: /bulk select/i,
+    });
+    // Bulk select should not appear without proper permissions
+    expect(bulkSelectButton).not.toBeInTheDocument();
+  });
+});
+
+test('handles network timeout without crashing', async () => {
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { throws: new Error('Network timeout') },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser, {
+    addDangerToast: jest.fn(),
+    addSuccessToast: jest.fn(),
+  });
+
+  // Component should not crash
+  await waitForDatasetsPageReady();
+});
+
+test('component requires explicit mocks for all API endpoints', async () => {
+  // Use standard mocks
+  setupMocks();
+
+  // Clear call history to start fresh
+  fetchMock.resetHistory();
+
+  // Render component with standard setup
+  renderDatasetList(mockAdminUser);
+
+  // Wait for initial data load
+  await waitForDatasetsPageReady();
+
+  // Verify that critical endpoints were called and had mocks available
+  const newDatasetsCalls = fetchMock.calls(API_ENDPOINTS.DATASETS);
+  const newInfoCalls = fetchMock.calls(API_ENDPOINTS.DATASETS_INFO);
+
+  // These should have been called during render
+  expect(newDatasetsCalls.length).toBeGreaterThan(0);
+  expect(newInfoCalls.length).toBeGreaterThan(0);
+
+  // Verify no unmatched calls (all endpoints were mocked)
+  const unmatchedCalls = fetchMock.calls(false); // false = unmatched only
+  expect(unmatchedCalls.length).toBe(0);
+});
+
+test('selecting Database filter triggers API call with database relation 
filter', async () => {
+  renderDatasetList(mockAdminUser);
+
+  await waitForDatasetsPageReady();
+
+  const filtersContainers = screen.getAllByRole('combobox');
+  expect(filtersContainers.length).toBeGreaterThan(0);
+});
+
+test('renders datasets with certification data', async () => {
+  const certifiedDataset = {
+    ...mockDatasets[1], // mockDatasets[1] has certification
+    extra: JSON.stringify({
+      certification: {
+        certified_by: 'Data Team',
+        details: 'Approved for production',
+      },
+    }),
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [certifiedDataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(certifiedDataset.table_name)).toBeInTheDocument();
+  });
+
+  // Verify the dataset row renders successfully
+  const datasetRow = screen
+    .getByText(certifiedDataset.table_name)
+    .closest('tr');
+  expect(datasetRow).toBeInTheDocument();
+});
+
+test('displays datasets with warning_markdown', async () => {
+  const warningText = 'This dataset contains PII. Handle with care.';
+  const datasetWithWarning = {
+    ...mockDatasets[2], // mockDatasets[2] has warning
+    extra: JSON.stringify({
+      warning_markdown: warningText,
+    }),
+  };
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [datasetWithWarning], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    
expect(screen.getByText(datasetWithWarning.table_name)).toBeInTheDocument();
+  });
+
+  // Verify the dataset row exists
+  const datasetRow = screen
+    .getByText(datasetWithWarning.table_name)
+    .closest('tr');
+  expect(datasetRow).toBeInTheDocument();
+});
+
+test('displays dataset with multiple owners', async () => {
+  const datasetWithOwners = mockDatasets[1]; // Has 2 owners: Jane Smith, Bob 
Jones
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [datasetWithOwners], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(datasetWithOwners.table_name)).toBeInTheDocument();
+  });
+
+  // Verify row exists with the dataset
+  const datasetRow = screen
+    .getByText(datasetWithOwners.table_name)
+    .closest('tr');
+  expect(datasetRow).toBeInTheDocument();
+});
+
+test('displays ModifiedInfo with humanized date', async () => {
+  const datasetWithModified = mockDatasets[0]; // changed_by_name: 'John Doe', 
changed_on: '1 day ago'
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [datasetWithModified], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(datasetWithModified.table_name),
+    ).toBeInTheDocument();
+  });
+
+  // Verify humanized date appears (ModifiedInfo component renders it)
+  expect(
+    screen.getByText(datasetWithModified.changed_on_delta_humanized),
+  ).toBeInTheDocument();
+});
+
+test('dataset name links to Explore with correct explore_url', async () => {
+  const dataset = mockDatasets[0]; // explore_url: 
'/explore/?datasource=1__table'
+
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS,
+    { result: [dataset], count: 1 },
+    { overwriteRoutes: true },
+  );
+
+  renderDatasetList(mockAdminUser);
+
+  await waitFor(() => {
+    expect(screen.getByText(dataset.table_name)).toBeInTheDocument();
+  });
+
+  // Find the dataset name link (should be a link role)
+  const exploreLink = screen.getByRole('link', { name: dataset.table_name });
+  expect(exploreLink).toBeInTheDocument();
+  expect(exploreLink).toHaveAttribute('href', dataset.explore_url);
+});
diff --git 
a/superset-frontend/src/pages/DatasetList/DatasetList.testHelpers.tsx 
b/superset-frontend/src/pages/DatasetList/DatasetList.testHelpers.tsx
new file mode 100644
index 0000000000..844ad8f916
--- /dev/null
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.testHelpers.tsx
@@ -0,0 +1,539 @@
+/**
+ * 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.
+ */
+// eslint-disable-next-line import/no-extraneous-dependencies
+import fetchMock from 'fetch-mock';
+import { render, screen } from 'spec/helpers/testing-library';
+import { Provider } from 'react-redux';
+import { MemoryRouter } from 'react-router-dom';
+import { configureStore } from '@reduxjs/toolkit';
+import { QueryParamProvider } from 'use-query-params';
+import DatasetList from 'src/pages/DatasetList';
+import handleResourceExport from 'src/utils/export';
+
+export const mockHandleResourceExport =
+  handleResourceExport as jest.MockedFunction<typeof handleResourceExport>;
+
+// Type definitions for test helpers
+export interface UserState {
+  userId: string | number;
+  firstName: string;
+  lastName: string;
+  [key: string]: unknown; // Allow additional properties like roles
+}
+
+export interface RisonFilter {
+  col: string;
+  opr: string;
+  value: string | number | boolean;
+}
+
+// Test-only dataset type that matches the VirtualDataset interface from 
index.tsx
+// Includes extra/sql fields that exist in actual API responses
+export interface DatasetFixture {
+  id: number;
+  table_name: string;
+  kind: string;
+  schema: string;
+  database: {
+    id: string;
+    database_name: string;
+  };
+  owners: Array<{ first_name: string; last_name: string; id: number }>;
+  changed_by_name: string;
+  changed_by: {
+    first_name: string;
+    last_name: string;
+    id: number;
+  };
+  changed_on_delta_humanized: string;
+  explore_url: string;
+  extra: string; // JSON-serialized metadata (always present in API)
+  sql: string | null; // SQL query for virtual datasets
+  description?: string; // Optional description field
+}
+
+interface StoreState {
+  user?: UserState;
+  common?: {
+    conf?: {
+      SUPERSET_WEBSERVER_TIMEOUT?: number;
+      PREVENT_UNSAFE_DEFAULT_URLS_ON_DATASET?: boolean;
+    };
+  };
+  datasets?: {
+    datasetList?: typeof mockDatasets;
+  };
+}
+
+interface DatasetListPropsOverrides {
+  addDangerToast?: (msg: string) => void;
+  addSuccessToast?: (msg: string) => void;
+  user?: UserState;
+}
+
+export const mockDatasets: DatasetFixture[] = [
+  {
+    id: 1,
+    table_name: 'public.sales_data',
+    kind: 'physical',
+    schema: 'public',
+    database: {
+      id: '1',
+      database_name: 'PostgreSQL',
+    },
+    owners: [{ first_name: 'John', last_name: 'Doe', id: 1 }],
+    changed_by_name: 'John Doe',
+    changed_by: {
+      first_name: 'John',
+      last_name: 'Doe',
+      id: 1,
+    },
+    changed_on_delta_humanized: '1 day ago',
+    explore_url: '/explore/?datasource=1__table',
+    extra: JSON.stringify({}),
+    sql: null,
+  },
+  {
+    id: 2,
+    table_name: 'Analytics Query',
+    kind: 'virtual',
+    schema: 'analytics',
+    database: {
+      id: '2',
+      database_name: 'MySQL',
+    },
+    owners: [
+      { first_name: 'Jane', last_name: 'Smith', id: 2 },
+      { first_name: 'Bob', last_name: 'Jones', id: 3 },
+    ],
+    changed_by_name: 'Jane Smith',
+    changed_by: {
+      first_name: 'Jane',
+      last_name: 'Smith',
+      id: 2,
+    },
+    changed_on_delta_humanized: '2 hours ago',
+    explore_url: '/explore/?datasource=2__table',
+    extra: JSON.stringify({
+      certification: {
+        certified_by: 'Data Team',
+        details: 'Approved for production use',
+      },
+    }),
+    sql: 'SELECT * FROM analytics_table WHERE date >= current_date - 30',
+  },
+  {
+    id: 3,
+    table_name: 'Customer Metrics',
+    kind: 'virtual',
+    schema: 'metrics',
+    database: {
+      id: '1',
+      database_name: 'PostgreSQL',
+    },
+    owners: [],
+    changed_by_name: 'System',
+    changed_by: {
+      first_name: 'System',
+      last_name: 'User',
+      id: 999,
+    },
+    changed_on_delta_humanized: '5 days ago',
+    explore_url: '/explore/?datasource=3__table',
+    extra: JSON.stringify({
+      warning_markdown: 'This dataset contains PII. Handle with care.',
+    }),
+    sql: 'SELECT customer_id, COUNT(*) FROM orders GROUP BY customer_id',
+  },
+  {
+    id: 4,
+    table_name: 'public.product_catalog',
+    kind: 'physical',
+    schema: 'public',
+    database: {
+      id: '3',
+      database_name: 'Redshift',
+    },
+    owners: [{ first_name: 'Alice', last_name: 'Johnson', id: 4 }],
+    changed_by_name: 'Alice Johnson',
+    changed_by: {
+      first_name: 'Alice',
+      last_name: 'Johnson',
+      id: 4,
+    },
+    changed_on_delta_humanized: '3 weeks ago',
+    explore_url: '/explore/?datasource=4__table',
+    extra: JSON.stringify({
+      certification: {
+        certified_by: 'QA Team',
+        details: 'Verified data quality',
+      },
+      warning_markdown: 'Data refreshed weekly on Sundays',
+    }),
+    sql: null,
+  },
+  {
+    id: 5,
+    table_name: 'Quarterly Report',
+    kind: 'virtual',
+    schema: 'reports',
+    database: {
+      id: '2',
+      database_name: 'MySQL',
+    },
+    owners: [
+      { first_name: 'Charlie', last_name: 'Brown', id: 5 },
+      { first_name: 'David', last_name: 'Lee', id: 6 },
+      { first_name: 'Eve', last_name: 'Taylor', id: 7 },
+      { first_name: 'Frank', last_name: 'Wilson', id: 8 },
+    ],
+    changed_by_name: 'Charlie Brown',
+    changed_by: {
+      first_name: 'Charlie',
+      last_name: 'Brown',
+      id: 5,
+    },
+    changed_on_delta_humanized: '1 month ago',
+    explore_url: '/explore/?datasource=5__table',
+    extra: JSON.stringify({}),
+    sql: 'SELECT quarter, SUM(revenue) FROM sales GROUP BY quarter',
+  },
+];
+
+// Mock users with various permission levels
+export const mockAdminUser = {
+  userId: 1,
+  firstName: 'Admin',
+  lastName: 'User',
+  roles: {
+    Admin: [
+      ['can_read', 'Dataset'],
+      ['can_write', 'Dataset'],
+      ['can_export', 'Dataset'],
+      ['can_duplicate', 'Dataset'],
+    ],
+  },
+};
+
+export const mockOwnerUser = {
+  userId: 1,
+  firstName: 'John',
+  lastName: 'Doe',
+  roles: {
+    Alpha: [
+      ['can_read', 'Dataset'],
+      ['can_write', 'Dataset'],
+      ['can_export', 'Dataset'],
+      ['can_duplicate', 'Dataset'],
+    ],
+  },
+};
+
+export const mockReadOnlyUser = {
+  userId: 10,
+  firstName: 'Read',
+  lastName: 'Only',
+  roles: {
+    Gamma: [['can_read', 'Dataset']],
+  },
+};
+
+export const mockExportOnlyUser = {
+  userId: 11,
+  firstName: 'Export',
+  lastName: 'User',
+  roles: {
+    Gamma: [
+      ['can_read', 'Dataset'],
+      ['can_export', 'Dataset'],
+    ],
+  },
+};
+
+export const mockWriteUser = {
+  userId: 9,
+  firstName: 'Write',
+  lastName: 'User',
+  roles: {
+    Alpha: [
+      ['can_read', 'Dataset'],
+      ['can_write', 'Dataset'],
+      ['can_export', 'Dataset'],
+    ],
+  },
+};
+
+// Mock related objects for delete modal
+export const mockRelatedCharts = {
+  count: 3,
+  result: [
+    { id: 101, slice_name: 'Sales Chart' },
+    { id: 102, slice_name: 'Revenue Chart' },
+    { id: 103, slice_name: 'Analytics Chart' },
+  ],
+};
+
+export const mockRelatedDashboards = {
+  count: 2,
+  result: [
+    { id: 201, title: 'Executive Dashboard' },
+    { id: 202, title: 'Sales Dashboard' },
+  ],
+};
+
+// Mock API error responses
+export const mockApiError500 = {
+  status: 500,
+  body: { message: 'Internal Server Error' },
+};
+
+export const mockApiError403 = {
+  status: 403,
+  body: { message: 'Forbidden' },
+};
+
+export const mockApiError404 = {
+  status: 404,
+  body: { message: 'Not Found' },
+};
+
+// API endpoint constants
+export const API_ENDPOINTS = {
+  DATASETS_INFO: 'glob:*/api/v1/dataset/_info*',
+  DATASETS: 'glob:*/api/v1/dataset/?*',
+  DATASET_GET: 'glob:*/api/v1/dataset/[0-9]*',
+  DATASET_RELATED_OBJECTS: 'glob:*/api/v1/dataset/*/related_objects*',
+  DATASET_DELETE: 'glob:*/api/v1/dataset/[0-9]*',
+  DATASET_BULK_DELETE: 'glob:*/api/v1/dataset/?q=*', // Matches DELETE 
/api/v1/dataset/?q=...
+  DATASET_DUPLICATE: 'glob:*/api/v1/dataset/duplicate*',
+  DATASET_FAVORITE_STATUS: 'glob:*/api/v1/dataset/favorite_status*',
+  DATASET_RELATED_DATABASE: 'glob:*/api/v1/dataset/related/database*',
+  DATASET_RELATED_SCHEMA: 'glob:*/api/v1/dataset/distinct/schema*',
+  DATASET_RELATED_OWNERS: 'glob:*/api/v1/dataset/related/owners*',
+  DATASET_RELATED_CHANGED_BY: 'glob:*/api/v1/dataset/related/changed_by*',
+};
+
+// Setup API permissions mock (for permission-based testing)
+export const setupApiPermissions = (permissions: string[]) => {
+  fetchMock.get(
+    API_ENDPOINTS.DATASETS_INFO,
+    { permissions },
+    { overwriteRoutes: true },
+  );
+};
+
+// Store utilities
+export const createMockStore = (initialState: Partial<StoreState> = {}) =>
+  configureStore({
+    reducer: {
+      user: (state = initialState.user || {}) => state,
+      common: (state = initialState.common || {}) => state,
+      datasets: (state = initialState.datasets || {}) => state,
+    },
+    preloadedState: initialState,
+    middleware: getDefaultMiddleware =>
+      getDefaultMiddleware({
+        serializableCheck: false,
+        immutableCheck: false,
+      }),
+  });
+
+export const createDefaultStoreState = (user: UserState): StoreState => ({
+  user,
+  common: {
+    conf: {
+      SUPERSET_WEBSERVER_TIMEOUT: 60000,
+      PREVENT_UNSAFE_DEFAULT_URLS_ON_DATASET: false,
+    },
+  },
+  datasets: {
+    datasetList: mockDatasets,
+  },
+});
+
+export const renderDatasetList = (
+  user: UserState,
+  props: Partial<DatasetListPropsOverrides> = {},
+  storeState: Partial<StoreState> = {},
+) => {
+  const defaultStoreState = createDefaultStoreState(user);
+  const storeStateWithUser = {
+    ...defaultStoreState,
+    user,
+    ...storeState,
+  };
+
+  const store = createMockStore(storeStateWithUser);
+
+  return render(
+    <Provider store={store}>
+      <MemoryRouter>
+        <QueryParamProvider>
+          <DatasetList user={user} {...props} />
+        </QueryParamProvider>
+      </MemoryRouter>
+    </Provider>,
+  );
+};
+
+/**
+ * Helper to wait for the DatasetList page to be ready
+ * Waits for the "Datasets" heading to appear, indicating initial render is 
complete
+ */
+export const waitForDatasetsPageReady = async () => {
+  await screen.findByText('Datasets');
+};
+
+// Helper functions for specific operations
+export const setupDeleteMocks = (datasetId: number) => {
+  fetchMock.get(
+    `glob:*/api/v1/dataset/${datasetId}/related_objects*`,
+    {
+      charts: mockRelatedCharts,
+      dashboards: mockRelatedDashboards,
+    },
+    { overwriteRoutes: true },
+  );
+
+  fetchMock.delete(
+    `glob:*/api/v1/dataset/${datasetId}`,
+    { message: 'Dataset deleted successfully' },
+    { overwriteRoutes: true },
+  );
+};
+
+export const setupDuplicateMocks = () => {
+  fetchMock.post(
+    API_ENDPOINTS.DATASET_DUPLICATE,
+    { id: 999, table_name: 'Copy of Dataset' },
+    { overwriteRoutes: true },
+  );
+};
+
+export const setupBulkDeleteMocks = () => {
+  fetchMock.delete(
+    API_ENDPOINTS.DATASET_BULK_DELETE,
+    { message: '3 datasets deleted successfully' },
+    { overwriteRoutes: true },
+  );
+};
+
+// Setup error mocks for negative flow testing
+export const setupDeleteErrorMocks = (
+  datasetId: number,
+  statusCode: number,
+) => {
+  fetchMock.get(
+    `glob:*/api/v1/dataset/${datasetId}/related_objects*`,
+    {
+      status: statusCode,
+      body: { message: 'Failed to fetch related objects' },
+    },
+    { overwriteRoutes: true },
+  );
+};
+
+export const setupDuplicateErrorMocks = (statusCode: number) => {
+  fetchMock.post(
+    API_ENDPOINTS.DATASET_DUPLICATE,
+    {
+      status: statusCode,
+      body: { message: 'Failed to duplicate dataset' },
+    },
+    { overwriteRoutes: true },
+  );
+};
+
+/**
+ * Helper function to verify only expected API calls were made
+ * Replaces global fail-fast fetchMock.catch() with test-specific assertions
+ *
+ * @param expectedEndpoints - Array of endpoint glob patterns that should have 
been called
+ * @throws If any unmocked endpoints were called or expected endpoints weren't 
called
+ */
+export const assertOnlyExpectedCalls = (expectedEndpoints: string[]) => {
+  const allCalls = fetchMock.calls(true); // Get all calls including unmatched
+  const unmatchedCalls = allCalls.filter(call => call.isUnmatched);
+
+  if (unmatchedCalls.length > 0) {
+    const unmatchedUrls = unmatchedCalls.map(call => call[0]);
+    throw new Error(
+      `Unmocked endpoints called: ${unmatchedUrls.join(', ')}. ` +
+        'Add explicit mocks in setupMocks() or test setup.',
+    );
+  }
+
+  // Verify expected endpoints were called
+  expectedEndpoints.forEach(endpoint => {
+    const calls = fetchMock.calls(endpoint);
+    if (calls.length === 0) {
+      throw new Error(
+        `Expected endpoint not called: ${endpoint}. ` +
+          'Check if component logic changed or mock is incorrectly 
configured.',
+      );
+    }
+  });
+};
+
+// MSW setup using fetch-mock (following ChartList pattern)
+export const setupMocks = () => {
+  fetchMock.reset();
+
+  fetchMock.get(API_ENDPOINTS.DATASETS_INFO, {
+    permissions: ['can_read', 'can_write', 'can_export', 'can_duplicate'],
+  });
+
+  fetchMock.get(API_ENDPOINTS.DATASETS, {
+    result: mockDatasets,
+    count: mockDatasets.length,
+  });
+
+  fetchMock.get(API_ENDPOINTS.DATASET_FAVORITE_STATUS, {
+    result: [],
+  });
+
+  fetchMock.get(API_ENDPOINTS.DATASET_RELATED_DATABASE, {
+    result: [
+      { value: 1, text: 'PostgreSQL' },
+      { value: 2, text: 'MySQL' },
+      { value: 3, text: 'Redshift' },
+    ],
+    count: 3,
+  });
+
+  fetchMock.get(API_ENDPOINTS.DATASET_RELATED_SCHEMA, {
+    result: [
+      { value: 'public', text: 'public' },
+      { value: 'analytics', text: 'analytics' },
+      { value: 'metrics', text: 'metrics' },
+      { value: 'reports', text: 'reports' },
+    ],
+    count: 4,
+  });
+
+  fetchMock.get(API_ENDPOINTS.DATASET_RELATED_OWNERS, {
+    result: [],
+    count: 0,
+  });
+
+  fetchMock.get(API_ENDPOINTS.DATASET_RELATED_CHANGED_BY, {
+    result: [],
+    count: 0,
+  });
+};

Reply via email to