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

jli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new c25adbc395 test(DashboardList): migrate Cypress E2E tests to RTL 
(#38368)
c25adbc395 is described below

commit c25adbc3953f09dc59730734c65326f9c42bbe9d
Author: Joe Li <[email protected]>
AuthorDate: Wed Mar 4 11:13:13 2026 -0800

    test(DashboardList): migrate Cypress E2E tests to RTL (#38368)
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../cypress/e2e/dashboard_list/filter.test.ts      |  47 ---
 .../cypress/e2e/dashboard_list/list.test.ts        | 279 --------------
 .../DashboardList/DashboardList.behavior.test.tsx  | 394 +++++++++++++++++++
 .../DashboardList/DashboardList.cardview.test.tsx  | 417 +++++++++++++++++++++
 .../DashboardList/DashboardList.listview.test.tsx  | 402 ++++++++++++++++++++
 .../DashboardList.permissions.test.tsx             | 340 +++++++++++++++++
 .../src/pages/DashboardList/DashboardList.test.tsx | 391 ++++++++++---------
 .../DashboardList/DashboardList.testHelpers.tsx    | 360 ++++++++++++++++++
 8 files changed, 2137 insertions(+), 493 deletions(-)

diff --git 
a/superset-frontend/cypress-base/cypress/e2e/dashboard_list/filter.test.ts 
b/superset-frontend/cypress-base/cypress/e2e/dashboard_list/filter.test.ts
deleted file mode 100644
index 854ea541c7..0000000000
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard_list/filter.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { DASHBOARD_LIST } from 'cypress/utils/urls';
-import { setGridMode, clearAllInputs } from 'cypress/utils';
-import { setFilter } from '../dashboard/utils';
-
-describe('Dashboards filters', () => {
-  before(() => {
-    cy.visit(DASHBOARD_LIST);
-    setGridMode('card');
-  });
-
-  beforeEach(() => {
-    clearAllInputs();
-  });
-
-  it('should allow filtering by "Owner" correctly', () => {
-    setFilter('Owner', 'alpha user');
-    setFilter('Owner', 'admin user');
-  });
-
-  it('should allow filtering by "Modified by" correctly', () => {
-    setFilter('Modified by', 'alpha user');
-    setFilter('Modified by', 'admin user');
-  });
-
-  it('should allow filtering by "Status" correctly', () => {
-    setFilter('Status', 'Published');
-    setFilter('Status', 'Draft');
-  });
-});
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/dashboard_list/list.test.ts 
b/superset-frontend/cypress-base/cypress/e2e/dashboard_list/list.test.ts
deleted file mode 100644
index 37a8a7ffa4..0000000000
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard_list/list.test.ts
+++ /dev/null
@@ -1,279 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { DASHBOARD_LIST } from 'cypress/utils/urls';
-import { setGridMode, toggleBulkSelect } from 'cypress/utils';
-import {
-  setFilter,
-  interceptBulkDelete,
-  interceptUpdate,
-  interceptDelete,
-  interceptFav,
-  interceptUnfav,
-} from '../dashboard/utils';
-
-function orderAlphabetical() {
-  setFilter('Sort', 'Alphabetical');
-}
-
-function openProperties() {
-  cy.get('[aria-label="more"]').first().click();
-  cy.getBySel('dashboard-card-option-edit-button').click();
-}
-
-function openMenu() {
-  cy.get('[aria-label="more"]').first().click();
-}
-
-function confirmDelete(bulk = false) {
-  interceptDelete();
-  interceptBulkDelete();
-
-  // Wait for modal dialog to be present and visible
-  cy.get('[role="dialog"][aria-modal="true"]').should('be.visible');
-  cy.getBySel('delete-modal-input')
-    .should('be.visible')
-    .then($input => {
-      cy.wrap($input).clear();
-      cy.wrap($input).type('DELETE');
-    });
-  cy.getBySel('modal-confirm-button').should('be.visible').click();
-
-  if (bulk) {
-    cy.wait('@bulkDelete');
-  } else {
-    cy.wait('@delete');
-  }
-}
-
-describe('Dashboards list', () => {
-  describe('list mode', () => {
-    before(() => {
-      cy.visit(DASHBOARD_LIST);
-      setGridMode('list');
-    });
-
-    it('should load rows in list mode', () => {
-      cy.getBySel('listview-table').should('be.visible');
-      cy.getBySel('sort-header').eq(1).contains('Name');
-      cy.getBySel('sort-header').eq(2).contains('Status');
-      cy.getBySel('sort-header').eq(3).contains('Owners');
-      cy.getBySel('sort-header').eq(4).contains('Last modified');
-      cy.getBySel('sort-header').eq(5).contains('Actions');
-    });
-
-    // Skipped: depends on specific example dashboards that may vary
-    it.skip('should sort correctly in list mode', () => {
-      cy.getBySel('sort-header').eq(1).click();
-      cy.getBySel('loading-indicator').should('not.exist');
-      cy.getBySel('table-row').first().contains('Supported Charts Dashboard');
-      cy.getBySel('sort-header').eq(1).click();
-      cy.getBySel('loading-indicator').should('not.exist');
-      cy.getBySel('table-row').first().contains("World Bank's Data");
-      cy.getBySel('sort-header').eq(1).click();
-    });
-
-    it('should bulk select in list mode', () => {
-      toggleBulkSelect();
-      cy.get('th.ant-table-cell input[aria-label="Select all"]').click();
-      // Check that checkboxes are checked (count varies based on loaded 
examples)
-      cy.get(
-        '.ant-checkbox-input:not(th.ant-table-measure-cell 
.ant-checkbox-input)',
-      )
-        .should('be.checked')
-        .should('have.length.at.least', 1);
-      cy.getBySel('bulk-select-copy').contains('Selected');
-      cy.getBySel('bulk-select-action')
-        .should('have.length', 2)
-        .then($btns => {
-          expect($btns).to.contain('Delete');
-          expect($btns).to.contain('Export');
-        });
-      cy.getBySel('bulk-select-deselect-all').click();
-      cy.get('input[type="checkbox"]:checked').should('have.length', 0);
-      cy.getBySel('bulk-select-copy').contains('0 Selected');
-      cy.getBySel('bulk-select-action').should('not.exist');
-    });
-  });
-
-  describe('card mode', () => {
-    before(() => {
-      cy.visit(DASHBOARD_LIST);
-      setGridMode('card');
-    });
-
-    it('should load rows in card mode', () => {
-      cy.getBySel('listview-table').should('not.exist');
-      // Check that we have some dashboard cards (count varies based on loaded 
examples)
-      cy.getBySel('styled-card').should('have.length.at.least', 1);
-    });
-
-    it('should bulk select in card mode', () => {
-      toggleBulkSelect();
-      cy.getBySel('styled-card').click({ multiple: true });
-      cy.getBySel('bulk-select-copy').contains('Selected');
-      cy.getBySel('bulk-select-action')
-        .should('have.length', 2)
-        .then($btns => {
-          expect($btns).to.contain('Delete');
-          expect($btns).to.contain('Export');
-        });
-      cy.getBySel('bulk-select-deselect-all').click();
-      cy.getBySel('bulk-select-copy').contains('0 Selected');
-      cy.getBySel('bulk-select-action').should('not.exist');
-    });
-
-    // Skipped: depends on specific example dashboards that may vary
-    it.skip('should sort in card mode', () => {
-      orderAlphabetical();
-      cy.getBySel('styled-card').first().contains('Supported Charts 
Dashboard');
-    });
-
-    it('should preserve other filters when sorting', () => {
-      // Check that we have some cards (count varies based on loaded examples)
-      cy.getBySel('styled-card').should('have.length.at.least', 1);
-      setFilter('Status', 'Published');
-      setFilter('Sort', 'Least recently modified');
-      // After filtering, we should have some cards (at least 1 if any are 
published)
-      cy.getBySel('styled-card').should('have.length.at.least', 1);
-    });
-  });
-
-  describe('common actions', () => {
-    beforeEach(() => {
-      cy.createSampleDashboards([0, 1, 2, 3]);
-      cy.visit(DASHBOARD_LIST);
-    });
-
-    it('should allow to favorite/unfavorite dashboard', () => {
-      interceptFav();
-      interceptUnfav();
-
-      setGridMode('card');
-      orderAlphabetical();
-
-      cy.getBySel('styled-card').first().contains('1 - Sample dashboard');
-      cy.getBySel('styled-card')
-        .first()
-        .find("[aria-label='unstarred']")
-        .click();
-      cy.wait('@select');
-      
cy.getBySel('styled-card').first().find("[aria-label='starred']").click();
-      cy.wait('@unselect');
-      cy.getBySel('styled-card')
-        .first()
-        .find("[aria-label='starred']")
-        .should('not.exist');
-    });
-
-    it('should bulk delete correctly', () => {
-      toggleBulkSelect();
-
-      // bulk deletes in card-view
-      setGridMode('card');
-      orderAlphabetical();
-
-      cy.getBySel('styled-card').eq(0).contains('1 - Sample 
dashboard').click();
-      cy.getBySel('styled-card').eq(1).contains('2 - Sample 
dashboard').click();
-      cy.getBySel('bulk-select-action').eq(0).contains('Delete').click();
-      confirmDelete(true);
-      cy.getBySel('styled-card')
-        .eq(0)
-        .should('not.contain', '1 - Sample dashboard');
-      cy.getBySel('styled-card')
-        .eq(1)
-        .should('not.contain', '2 - Sample dashboard');
-
-      // bulk deletes in list-view
-      setGridMode('list');
-      cy.getBySel('table-row').eq(0).contains('3 - Sample dashboard');
-      cy.getBySel('table-row').eq(1).contains('4 - Sample dashboard');
-      cy.get('[data-test="table-row"] input[type="checkbox"]').eq(0).click();
-      cy.get('[data-test="table-row"] input[type="checkbox"]').eq(1).click();
-      cy.getBySel('bulk-select-action').eq(0).contains('Delete').click();
-      confirmDelete(true);
-      cy.getBySel('loading-indicator').should('exist');
-      cy.getBySel('loading-indicator').should('not.exist');
-      cy.getBySel('table-row')
-        .eq(0)
-        .should('not.contain', '3 - Sample dashboard');
-      cy.getBySel('table-row')
-        .eq(1)
-        .should('not.contain', '4 - Sample dashboard');
-    });
-
-    it.skip('should delete correctly in list mode', () => {
-      // deletes in list-view
-      setGridMode('list');
-
-      cy.getBySel('table-row')
-        .eq(0)
-        .contains('4 - Sample dashboard')
-        .should('exist');
-      cy.getBySel('dashboard-list-trash-icon').eq(0).click();
-      confirmDelete();
-      cy.getBySel('table-row')
-        .eq(0)
-        .should('not.contain', '4 - Sample dashboard');
-    });
-
-    it('should delete correctly in card mode', () => {
-      // deletes in card-view
-      setGridMode('card');
-      orderAlphabetical();
-
-      cy.getBySel('styled-card')
-        .eq(0)
-        .contains('1 - Sample dashboard')
-        .should('exist');
-      openMenu();
-      cy.getBySel('dashboard-card-option-delete-button').click();
-      confirmDelete();
-      cy.getBySel('styled-card')
-        .eq(0)
-        .should('not.contain', '1 - Sample dashboard');
-    });
-
-    it('should edit correctly', () => {
-      interceptUpdate();
-
-      // edits in card-view
-      setGridMode('card');
-      orderAlphabetical();
-      cy.getBySel('styled-card').eq(0).contains('1 - Sample dashboard');
-
-      // change title
-      openProperties();
-      cy.getBySel('dashboard-title-input').type(' | EDITED');
-      cy.get('button:contains("Save")').click();
-      cy.wait('@update');
-      cy.getBySel('styled-card')
-        .eq(0)
-        .contains('1 - Sample dashboard | EDITED');
-
-      // edits in list-view
-      setGridMode('list');
-      cy.getBySel('edit-alt').eq(0).click();
-      cy.getBySel('dashboard-title-input').clear();
-      cy.getBySel('dashboard-title-input').type('1 - Sample dashboard');
-      cy.get('button:contains("Save")').click();
-      cy.wait('@update');
-      cy.getBySel('table-row').eq(0).contains('1 - Sample dashboard');
-    });
-  });
-});
diff --git 
a/superset-frontend/src/pages/DashboardList/DashboardList.behavior.test.tsx 
b/superset-frontend/src/pages/DashboardList/DashboardList.behavior.test.tsx
new file mode 100644
index 0000000000..997bca9fd8
--- /dev/null
+++ b/superset-frontend/src/pages/DashboardList/DashboardList.behavior.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 fetchMock from 'fetch-mock';
+import { fireEvent, screen, waitFor } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { isFeatureEnabled } from '@superset-ui/core';
+import {
+  API_ENDPOINTS,
+  mockDashboards,
+  setupMocks,
+  renderDashboardList,
+} from './DashboardList.testHelpers';
+
+jest.setTimeout(30000);
+
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  isFeatureEnabled: jest.fn(),
+}));
+
+jest.mock('src/utils/export', () => ({
+  __esModule: true,
+  default: jest.fn(),
+}));
+
+const mockIsFeatureEnabled = isFeatureEnabled as jest.MockedFunction<
+  typeof isFeatureEnabled
+>;
+
+const mockUser = {
+  userId: 1,
+  firstName: 'Test',
+  lastName: 'User',
+  roles: {
+    Admin: [
+      ['can_write', 'Dashboard'],
+      ['can_export', 'Dashboard'],
+    ],
+  },
+};
+
+beforeEach(() => {
+  setupMocks();
+  // Default to card view for behavior tests
+  mockIsFeatureEnabled.mockImplementation(
+    (feature: string) => feature === 'LISTVIEWS_DEFAULT_CARD_VIEW',
+  );
+});
+
+afterEach(() => {
+  fetchMock.clearHistory().removeRoutes();
+  mockIsFeatureEnabled.mockReset();
+});
+
+test('can favorite a dashboard', async () => {
+  // Mock favorite status - dashboard 1 is not favorited
+  fetchMock.removeRoutes({
+    names: ['glob:*/api/v1/dashboard/favorite_status*'],
+  });
+  fetchMock.get('glob:*/api/v1/dashboard/favorite_status*', {
+    result: mockDashboards.map(d => ({
+      id: d.id,
+      value: false,
+    })),
+  });
+
+  // Mock the POST to favorite endpoint
+  fetchMock.post('glob:*/api/v1/dashboard/*/favorites/', {
+    result: 'OK',
+  });
+
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(mockDashboards[0].dashboard_title),
+    ).toBeInTheDocument();
+  });
+
+  // Find and click an unstarred favorite icon
+  const favoriteIcons = screen.getAllByTestId('fave-unfave-icon');
+  expect(favoriteIcons.length).toBeGreaterThan(0);
+  fireEvent.click(favoriteIcons[0]);
+
+  // Verify POST was made to favorites endpoint
+  await waitFor(() => {
+    const favCalls = fetchMock.callHistory.calls(/dashboard\/\d+\/favorites/, {
+      method: 'POST',
+    });
+    expect(favCalls).toHaveLength(1);
+  });
+
+  // Verify the star icon flipped to starred state
+  await waitFor(() => {
+    expect(screen.getByRole('img', { name: 'starred' })).toBeInTheDocument();
+  });
+});
+
+test('can unfavorite a dashboard', async () => {
+  // Clear all routes and re-setup with favorited dashboard
+  fetchMock.clearHistory().removeRoutes();
+
+  // Setup mocks manually with dashboard 1 favorited
+  fetchMock.get('glob:*/api/v1/dashboard/_info*', {
+    permissions: ['can_read', 'can_write', 'can_export'],
+  });
+  fetchMock.get('glob:*/api/v1/dashboard/?*', {
+    result: mockDashboards,
+    dashboard_count: mockDashboards.length,
+  });
+  fetchMock.get('glob:*/api/v1/dashboard/favorite_status*', {
+    result: mockDashboards.map(d => ({
+      id: d.id,
+      value: d.id === 1,
+    })),
+  });
+  fetchMock.get('glob:*/api/v1/dashboard/related/owners*', {
+    result: [],
+    count: 0,
+  });
+  fetchMock.get('glob:*/api/v1/dashboard/related/changed_by*', {
+    result: [],
+    count: 0,
+  });
+  global.URL.createObjectURL = jest.fn();
+  fetchMock.get('/thumbnail', { body: new Blob(), sendAsJson: false });
+  fetchMock.get('glob:*', (callLog: any) => {
+    const reqUrl =
+      typeof callLog === 'string' ? callLog : callLog?.url || callLog;
+    throw new Error(`[fetchMock catch-all] Unmatched GET: ${reqUrl}`);
+  });
+
+  // Mock the DELETE to unfavorite endpoint
+  fetchMock.delete('glob:*/api/v1/dashboard/*/favorites/', {
+    result: 'OK',
+  });
+
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(mockDashboards[0].dashboard_title),
+    ).toBeInTheDocument();
+  });
+
+  // Wait for the starred icon to appear (favorite status loaded)
+  const starredIcon = await screen.findByRole('img', { name: 'starred' });
+  fireEvent.click(starredIcon);
+
+  // Verify DELETE was made to favorites endpoint
+  await waitFor(() => {
+    const unfavCalls = fetchMock.callHistory.calls(
+      /dashboard\/\d+\/favorites/,
+      { method: 'DELETE' },
+    );
+    expect(unfavCalls).toHaveLength(1);
+  });
+
+  // Verify the star icon flipped back to unstarred state
+  await waitFor(() => {
+    expect(
+      screen.queryByRole('img', { name: 'starred' }),
+    ).not.toBeInTheDocument();
+  });
+});
+
+test('can delete a single dashboard from card menu', async () => {
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(mockDashboards[0].dashboard_title),
+    ).toBeInTheDocument();
+  });
+
+  // Open card menu
+  const moreButtons = screen.getAllByLabelText('more');
+  fireEvent.click(moreButtons[0]);
+
+  // Click delete from the dropdown
+  const deleteButton = await screen.findByTestId(
+    'dashboard-card-option-delete-button',
+  );
+  fireEvent.click(deleteButton);
+
+  // Should open delete confirmation dialog
+  await waitFor(() => {
+    expect(
+      screen.getByText(/Are you sure you want to delete/i),
+    ).toBeInTheDocument();
+  });
+
+  // Type DELETE in the confirmation input
+  const deleteInput = screen.getByTestId('delete-modal-input');
+  await userEvent.type(deleteInput, 'DELETE');
+
+  // Mock the DELETE endpoint
+  fetchMock.delete('glob:*/api/v1/dashboard/*', {
+    message: 'Dashboard deleted',
+  });
+
+  // Click confirm button
+  const confirmButton = screen.getByTestId('modal-confirm-button');
+  fireEvent.click(confirmButton);
+
+  // Verify delete API was called
+  await waitFor(() => {
+    const deleteCalls = fetchMock.callHistory.calls(/api\/v1\/dashboard\//, {
+      method: 'DELETE',
+    });
+    expect(deleteCalls).toHaveLength(1);
+  });
+
+  // Verify the delete confirmation dialog closes
+  await waitFor(() => {
+    expect(
+      screen.queryByText(/Are you sure you want to delete/i),
+    ).not.toBeInTheDocument();
+  });
+});
+
+test('can edit dashboard title via properties modal', async () => {
+  // Clear all routes and re-setup with single dashboard mock
+  fetchMock.clearHistory().removeRoutes();
+
+  fetchMock.get(API_ENDPOINTS.DASHBOARDS_INFO, {
+    permissions: ['can_read', 'can_write', 'can_export'],
+  });
+  fetchMock.get(API_ENDPOINTS.DASHBOARDS, {
+    result: mockDashboards,
+    dashboard_count: mockDashboards.length,
+  });
+  fetchMock.get(API_ENDPOINTS.DASHBOARD_FAVORITE_STATUS, { result: [] });
+  fetchMock.get(API_ENDPOINTS.DASHBOARD_RELATED_OWNERS, {
+    result: [],
+    count: 0,
+  });
+  fetchMock.get(API_ENDPOINTS.DASHBOARD_RELATED_CHANGED_BY, {
+    result: [],
+    count: 0,
+  });
+  global.URL.createObjectURL = jest.fn();
+  fetchMock.get(API_ENDPOINTS.THUMBNAIL, {
+    body: new Blob(),
+    sendAsJson: false,
+  });
+
+  // Mock GET for single dashboard (PropertiesModal fetches 
/api/v1/dashboard/<id>)
+  fetchMock.get(/\/api\/v1\/dashboard\/\d+$/, {
+    result: {
+      ...mockDashboards[0],
+      json_metadata: '{}',
+      slug: '',
+      css: '',
+      is_managed_externally: false,
+      metadata: {},
+      theme: null,
+    },
+  });
+
+  // Mock themes endpoint (PropertiesModal fetches available themes)
+  fetchMock.get('glob:*/api/v1/theme/*', { result: [] });
+
+  // Catch-all must be last — fail hard on unmatched URLs
+  fetchMock.get(API_ENDPOINTS.CATCH_ALL, (callLog: any) => {
+    const reqUrl =
+      typeof callLog === 'string' ? callLog : callLog?.url || callLog;
+    throw new Error(`[fetchMock catch-all] Unmatched GET: ${reqUrl}`);
+  });
+
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(mockDashboards[0].dashboard_title),
+    ).toBeInTheDocument();
+  });
+
+  // Open card menu and click edit
+  const moreButtons = screen.getAllByLabelText('more');
+  fireEvent.click(moreButtons[0]);
+
+  const editButton = await screen.findByTestId(
+    'dashboard-card-option-edit-button',
+  );
+  fireEvent.click(editButton);
+
+  // Wait for properties modal to load and show the title input
+  const titleInput = await screen.findByTestId('dashboard-title-input');
+  expect(titleInput).toHaveValue(mockDashboards[0].dashboard_title);
+
+  // Change the title
+  await userEvent.clear(titleInput);
+  await userEvent.type(titleInput, 'Updated Dashboard Title');
+
+  // Mock the PUT endpoint
+  fetchMock.put('glob:*/api/v1/dashboard/*', {
+    result: {
+      ...mockDashboards[0],
+      dashboard_title: 'Updated Dashboard Title',
+    },
+  });
+
+  // Click Save button
+  const saveButton = screen.getByRole('button', { name: /save/i });
+  fireEvent.click(saveButton);
+
+  // Verify PUT API was called
+  await waitFor(() => {
+    const putCalls = fetchMock.callHistory.calls(/api\/v1\/dashboard\//, {
+      method: 'PUT',
+    });
+    expect(putCalls).toHaveLength(1);
+  });
+
+  // Verify the properties modal closes after save
+  await waitFor(() => {
+    expect(
+      screen.queryByTestId('dashboard-title-input'),
+    ).not.toBeInTheDocument();
+  });
+});
+
+test('opens delete confirmation from list view trash icon', async () => {
+  // Switch to list view
+  mockIsFeatureEnabled.mockReturnValue(false);
+
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(mockDashboards[0].dashboard_title),
+    ).toBeInTheDocument();
+  });
+
+  // Click the delete icon in the actions column
+  const trashIcons = screen.getAllByTestId('dashboard-list-trash-icon');
+  fireEvent.click(trashIcons[0]);
+
+  // Should open confirmation dialog
+  await waitFor(() => {
+    expect(
+      screen.getByText(/Are you sure you want to delete/i),
+    ).toBeInTheDocument();
+  });
+
+  // Type DELETE in the confirmation input
+  const deleteInput = screen.getByTestId('delete-modal-input');
+  await userEvent.type(deleteInput, 'DELETE');
+
+  // Mock the DELETE endpoint
+  fetchMock.delete('glob:*/api/v1/dashboard/*', {
+    message: 'Dashboard deleted',
+  });
+
+  // Click confirm button
+  const confirmButton = screen.getByTestId('modal-confirm-button');
+  fireEvent.click(confirmButton);
+
+  // Verify delete API was called
+  await waitFor(() => {
+    const deleteCalls = fetchMock.callHistory.calls(/api\/v1\/dashboard\//, {
+      method: 'DELETE',
+    });
+    expect(deleteCalls).toHaveLength(1);
+  });
+
+  // Verify the delete confirmation dialog closes
+  await waitFor(() => {
+    expect(
+      screen.queryByText(/Are you sure you want to delete/i),
+    ).not.toBeInTheDocument();
+  });
+});
diff --git 
a/superset-frontend/src/pages/DashboardList/DashboardList.cardview.test.tsx 
b/superset-frontend/src/pages/DashboardList/DashboardList.cardview.test.tsx
new file mode 100644
index 0000000000..c03299cfc4
--- /dev/null
+++ b/superset-frontend/src/pages/DashboardList/DashboardList.cardview.test.tsx
@@ -0,0 +1,417 @@
+/**
+ * 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 fetchMock from 'fetch-mock';
+import {
+  fireEvent,
+  screen,
+  waitFor,
+  within,
+} from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { isFeatureEnabled } from '@superset-ui/core';
+import {
+  mockDashboards,
+  mockHandleResourceExport,
+  renderDashboardList,
+  setupMocks,
+  getLatestDashboardApiCall,
+} from './DashboardList.testHelpers';
+
+jest.setTimeout(30000);
+
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  isFeatureEnabled: jest.fn(),
+}));
+
+jest.mock('src/utils/export', () => ({
+  __esModule: true,
+  default: jest.fn(),
+}));
+
+const mockUser = {
+  userId: 1,
+  firstName: 'Test',
+  lastName: 'User',
+  roles: {
+    Admin: [
+      ['can_write', 'Dashboard'],
+      ['can_export', 'Dashboard'],
+    ],
+  },
+};
+
+// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
+describe('DashboardList Card View Tests', () => {
+  beforeEach(() => {
+    setupMocks();
+
+    // Enable card view as default
+    (
+      isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
+    ).mockImplementation(
+      (feature: string) => feature === 'LISTVIEWS_DEFAULT_CARD_VIEW',
+    );
+  });
+
+  afterEach(() => fetchMock.clearHistory().removeRoutes());
+
+  test('renders cards instead of table', async () => {
+    renderDashboardList(mockUser);
+
+    await screen.findByTestId('dashboard-list-view');
+
+    // Verify no table in card view
+    expect(screen.queryByTestId('listview-table')).not.toBeInTheDocument();
+
+    // Verify card view toggle is active
+    const cardViewToggle = screen.getByRole('img', { name: 'appstore' });
+    const cardViewButton = cardViewToggle.closest('[role="button"]');
+    expect(cardViewButton).toHaveClass('active');
+  });
+
+  test('switches from card view to list view', async () => {
+    renderDashboardList(mockUser);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(screen.queryByTestId('listview-table')).not.toBeInTheDocument();
+
+    // Switch to list view
+    const listViewToggle = screen.getByRole('img', {
+      name: 'unordered-list',
+    });
+    const listViewButton = listViewToggle.closest('[role="button"]');
+    expect(listViewButton).not.toBeNull();
+    fireEvent.click(listViewButton!);
+
+    await waitFor(() => {
+      expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+    });
+  });
+
+  test('displays dashboard data correctly in cards', async () => {
+    renderDashboardList(mockUser);
+
+    await screen.findByTestId('dashboard-list-view');
+
+    await waitFor(() => {
+      expect(
+        screen.getByText(mockDashboards[0].dashboard_title),
+      ).toBeInTheDocument();
+    });
+
+    // Verify favorite stars exist (one per dashboard)
+    const favoriteStars = screen.getAllByTestId('fave-unfave-icon');
+    expect(favoriteStars).toHaveLength(mockDashboards.length);
+
+    // Verify action menu exists (more button for each card)
+    const moreButtons = screen.getAllByLabelText('more');
+    expect(moreButtons).toHaveLength(mockDashboards.length);
+
+    // Verify menu items appear on click
+    fireEvent.click(moreButtons[0]);
+    await waitFor(() => {
+      expect(screen.getByText('Edit')).toBeInTheDocument();
+      expect(screen.getByText('Export')).toBeInTheDocument();
+      expect(screen.getByText('Delete')).toBeInTheDocument();
+    });
+  });
+
+  test('renders sort dropdown in card view', async () => {
+    renderDashboardList(mockUser);
+    await screen.findByTestId('dashboard-list-view');
+
+    await waitFor(() => {
+      expect(screen.queryByTestId('listview-table')).not.toBeInTheDocument();
+    });
+
+    const sortFilter = screen.getByTestId('card-sort-select');
+    expect(sortFilter).toBeInTheDocument();
+    expect(sortFilter).toBeVisible();
+  });
+
+  test('selecting a sort option triggers new API call', async () => {
+    renderDashboardList(mockUser);
+    await screen.findByTestId('dashboard-list-view');
+
+    await waitFor(() => {
+      expect(
+        screen.getByText(mockDashboards[0].dashboard_title),
+      ).toBeInTheDocument();
+    });
+
+    // Find the sort select by its testId, then the combobox within it
+    const sortContainer = screen.getByTestId('card-sort-select');
+    const sortCombobox = within(sortContainer).getByRole('combobox');
+    await userEvent.click(sortCombobox);
+
+    // Select "Alphabetical" from the dropdown
+    const alphabeticalOption = await waitFor(() =>
+      within(
+        // eslint-disable-next-line testing-library/no-node-access
+        document.querySelector('.rc-virtual-list')!,
+      ).getByText('Alphabetical'),
+    );
+    await userEvent.click(alphabeticalOption);
+
+    await waitFor(() => {
+      const latest = getLatestDashboardApiCall();
+      expect(latest).not.toBeNull();
+      expect(latest!.query).toMatchObject({
+        order_column: 'dashboard_title',
+        order_direction: 'asc',
+      });
+    });
+  });
+
+  test('can bulk deselect all dashboards', async () => {
+    renderDashboardList(mockUser);
+
+    await screen.findByTestId('dashboard-list-view');
+    await waitFor(() => {
+      expect(
+        screen.getByText(mockDashboards[0].dashboard_title),
+      ).toBeInTheDocument();
+    });
+
+    // Enable bulk select mode
+    const bulkSelectButton = screen.getByTestId('bulk-select');
+    fireEvent.click(bulkSelectButton);
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
+    });
+
+    // Select first card
+    const firstDashboardName = screen.getByText(
+      mockDashboards[0].dashboard_title,
+    );
+    fireEvent.click(firstDashboardName);
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+        '1 Selected',
+      );
+    });
+
+    // Select second card
+    const secondDashboardName = screen.getByText(
+      mockDashboards[1].dashboard_title,
+    );
+    fireEvent.click(secondDashboardName);
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+        '2 Selected',
+      );
+    });
+
+    // Verify Delete and Export buttons appear
+    const bulkActions = screen.getAllByTestId('bulk-select-action');
+    expect(bulkActions.find(btn => btn.textContent === 'Delete')).toBeTruthy();
+    expect(bulkActions.find(btn => btn.textContent === 'Export')).toBeTruthy();
+
+    // Click deselect all
+    const deselectAllButton = screen.getByTestId('bulk-select-deselect-all');
+    fireEvent.click(deselectAllButton);
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+        '0 Selected',
+      );
+    });
+
+    // Bulk action buttons should disappear
+    expect(screen.queryByTestId('bulk-select-action')).not.toBeInTheDocument();
+  });
+
+  test('can bulk export selected dashboards', async () => {
+    renderDashboardList(mockUser);
+
+    await screen.findByTestId('dashboard-list-view');
+    await waitFor(() => {
+      expect(
+        screen.getByText(mockDashboards[0].dashboard_title),
+      ).toBeInTheDocument();
+    });
+
+    // Enable bulk select mode
+    const bulkSelectButton = screen.getByTestId('bulk-select');
+    fireEvent.click(bulkSelectButton);
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
+    });
+
+    // Select dashboards by clicking on each card
+    for (let i = 0; i < mockDashboards.length; i += 1) {
+      const dashboardName = 
screen.getByText(mockDashboards[i].dashboard_title);
+      fireEvent.click(dashboardName);
+    }
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+        `${mockDashboards.length} Selected`,
+      );
+    });
+
+    const bulkExportButton = screen.getByText('Export');
+    fireEvent.click(bulkExportButton);
+
+    expect(mockHandleResourceExport).toHaveBeenCalledWith(
+      'dashboard',
+      mockDashboards.map(d => d.id),
+      expect.any(Function),
+    );
+  });
+
+  test('can bulk delete selected dashboards', async () => {
+    renderDashboardList(mockUser);
+
+    await screen.findByTestId('dashboard-list-view');
+    await waitFor(() => {
+      expect(
+        screen.getByText(mockDashboards[0].dashboard_title),
+      ).toBeInTheDocument();
+    });
+
+    // Enable bulk select mode
+    const bulkSelectButton = screen.getByTestId('bulk-select');
+    fireEvent.click(bulkSelectButton);
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
+    });
+
+    // Select dashboards
+    for (let i = 0; i < mockDashboards.length; i += 1) {
+      const dashboardName = 
screen.getByText(mockDashboards[i].dashboard_title);
+      fireEvent.click(dashboardName);
+    }
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+        `${mockDashboards.length} Selected`,
+      );
+    });
+
+    const bulkDeleteButton = screen.getByText('Delete');
+    fireEvent.click(bulkDeleteButton);
+
+    await waitFor(() => {
+      expect(screen.getByText('Please confirm')).toBeInTheDocument();
+    });
+
+    // Type DELETE in the confirmation input
+    const deleteInput = screen.getByTestId('delete-modal-input');
+    await userEvent.type(deleteInput, 'DELETE');
+
+    // Mock the bulk DELETE endpoint
+    fetchMock.delete('glob:*/api/v1/dashboard/?*', {
+      message: 'Dashboards deleted',
+    });
+
+    // Click confirm button
+    const confirmButton = screen.getByTestId('modal-confirm-button');
+    fireEvent.click(confirmButton);
+
+    // Verify bulk delete API was called
+    await waitFor(() => {
+      const deleteCalls = fetchMock.callHistory.calls(
+        /api\/v1\/dashboard\/\?/,
+        { method: 'DELETE' },
+      );
+      expect(deleteCalls).toHaveLength(1);
+    });
+  });
+
+  test('exit bulk select by hitting x on bulk select bar', async () => {
+    renderDashboardList(mockUser);
+
+    await screen.findByTestId('dashboard-list-view');
+    await waitFor(() => {
+      expect(
+        screen.getByText(mockDashboards[0].dashboard_title),
+      ).toBeInTheDocument();
+    });
+
+    const bulkSelectButton = screen.getByTestId('bulk-select');
+    fireEvent.click(bulkSelectButton);
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
+    });
+
+    // Click the X button to close bulk select
+    const bulkSelectBar = screen.getByTestId('bulk-select-controls');
+    const closeButton = within(bulkSelectBar).getByRole('button', {
+      name: /close/i,
+    });
+    fireEvent.click(closeButton);
+
+    await waitFor(() => {
+      expect(
+        screen.queryByTestId('bulk-select-controls'),
+      ).not.toBeInTheDocument();
+    });
+  });
+
+  test('card click behavior changes in bulk select mode', async () => {
+    renderDashboardList(mockUser);
+
+    await screen.findByTestId('dashboard-list-view');
+    await waitFor(() => {
+      expect(
+        screen.getByText(mockDashboards[0].dashboard_title),
+      ).toBeInTheDocument();
+    });
+
+    expect(
+      screen.queryByTestId('bulk-select-controls'),
+    ).not.toBeInTheDocument();
+
+    const bulkSelectButton = screen.getByTestId('bulk-select');
+    fireEvent.click(bulkSelectButton);
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
+    });
+
+    // Clicking on cards should select them
+    const firstDashboardName = screen.getByText(
+      mockDashboards[0].dashboard_title,
+    );
+    fireEvent.click(firstDashboardName);
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+        '1 Selected',
+      );
+    });
+
+    // Clicking the same card again should deselect it
+    fireEvent.click(firstDashboardName);
+
+    await waitFor(() => {
+      expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+        '0 Selected',
+      );
+    });
+  });
+});
diff --git 
a/superset-frontend/src/pages/DashboardList/DashboardList.listview.test.tsx 
b/superset-frontend/src/pages/DashboardList/DashboardList.listview.test.tsx
new file mode 100644
index 0000000000..1030faf96c
--- /dev/null
+++ b/superset-frontend/src/pages/DashboardList/DashboardList.listview.test.tsx
@@ -0,0 +1,402 @@
+/**
+ * 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 fetchMock from 'fetch-mock';
+import {
+  fireEvent,
+  screen,
+  waitFor,
+  within,
+} from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { isFeatureEnabled } from '@superset-ui/core';
+import {
+  mockDashboards,
+  mockHandleResourceExport,
+  setupMocks,
+  renderDashboardList,
+  getLatestDashboardApiCall,
+} from './DashboardList.testHelpers';
+
+jest.setTimeout(30000);
+
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  isFeatureEnabled: jest.fn(),
+}));
+
+jest.mock('src/utils/export', () => ({
+  __esModule: true,
+  default: jest.fn(),
+}));
+
+const mockIsFeatureEnabled = isFeatureEnabled as jest.MockedFunction<
+  typeof isFeatureEnabled
+>;
+
+const mockUser = {
+  userId: 1,
+  firstName: 'Test',
+  lastName: 'User',
+  roles: {
+    Admin: [
+      ['can_write', 'Dashboard'],
+      ['can_export', 'Dashboard'],
+    ],
+  },
+};
+
+beforeEach(() => {
+  mockHandleResourceExport.mockClear();
+  setupMocks();
+  // Default to list view (no card view feature flag)
+  mockIsFeatureEnabled.mockReturnValue(false);
+});
+
+afterEach(() => {
+  fetchMock.clearHistory().removeRoutes();
+  mockIsFeatureEnabled.mockReset();
+});
+
+test('renders table in list view', async () => {
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('dashboard-list-view')).toBeInTheDocument();
+  });
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  expect(screen.queryByTestId('styled-card')).not.toBeInTheDocument();
+});
+
+test('renders all required column headers', async () => {
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+
+  const expectedHeaders = [
+    'Name',
+    'Status',
+    'Owners',
+    'Last modified',
+    'Actions',
+  ];
+
+  expectedHeaders.forEach(headerText => {
+    expect(within(table).getByTitle(headerText)).toBeInTheDocument();
+  });
+});
+
+test('displays dashboard data in table rows', async () => {
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const testDashboard = mockDashboards[0];
+
+  await waitFor(() => {
+    expect(
+      within(table).getByText(testDashboard.dashboard_title),
+    ).toBeInTheDocument();
+  });
+
+  // Find the specific row
+  const dashboardNameElement = within(table).getByText(
+    testDashboard.dashboard_title,
+  );
+  const dashboardRow = dashboardNameElement.closest(
+    '[data-test="table-row"]',
+  ) as HTMLElement;
+  expect(dashboardRow).toBeInTheDocument();
+
+  // Check for favorite star
+  const favoriteButton = within(dashboardRow).getByTestId('fave-unfave-icon');
+  expect(favoriteButton).toBeInTheDocument();
+
+  // Check last modified time
+  expect(
+    within(dashboardRow).getByText(testDashboard.changed_on_delta_humanized),
+  ).toBeInTheDocument();
+
+  // Verify action buttons exist
+  expect(within(dashboardRow).getByTestId('edit-alt')).toBeInTheDocument();
+});
+
+test('sorts table when clicking column header', async () => {
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const nameHeader = within(table).getByTitle('Name');
+  await userEvent.click(nameHeader);
+
+  await waitFor(() => {
+    const latest = getLatestDashboardApiCall();
+    expect(latest).not.toBeNull();
+    expect(latest!.query).toMatchObject({
+      order_column: 'dashboard_title',
+      order_direction: 'asc',
+    });
+  });
+});
+
+test('supports bulk select and deselect all', async () => {
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(mockDashboards[0].dashboard_title),
+    ).toBeInTheDocument();
+  });
+
+  const bulkSelectButton = screen.getByTestId('bulk-select');
+  await userEvent.click(bulkSelectButton);
+
+  await waitFor(() => {
+    expect(screen.getAllByRole('checkbox')).toHaveLength(
+      mockDashboards.length + 1,
+    );
+  });
+
+  // Select all
+  const selectAllCheckbox = screen.getAllByLabelText('Select all')[0];
+  expect(selectAllCheckbox).not.toBeChecked();
+  await userEvent.click(selectAllCheckbox);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+      `${mockDashboards.length} Selected`,
+    );
+  });
+
+  // Verify Delete and Export buttons appear
+  const bulkActions = screen.getAllByTestId('bulk-select-action');
+  expect(bulkActions.find(btn => btn.textContent === 'Delete')).toBeTruthy();
+  expect(bulkActions.find(btn => btn.textContent === 'Export')).toBeTruthy();
+
+  // Deselect all
+  const deselectAllButton = screen.getByTestId('bulk-select-deselect-all');
+  await userEvent.click(deselectAllButton);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+      '0 Selected',
+    );
+  });
+
+  // Bulk action buttons should disappear
+  expect(screen.queryByTestId('bulk-select-action')).not.toBeInTheDocument();
+});
+
+test('supports bulk export of selected dashboards', async () => {
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(mockDashboards[0].dashboard_title),
+    ).toBeInTheDocument();
+  });
+
+  const bulkSelectButton = screen.getByTestId('bulk-select');
+  await userEvent.click(bulkSelectButton);
+
+  await waitFor(() => {
+    expect(screen.getAllByRole('checkbox')).toHaveLength(
+      mockDashboards.length + 1,
+    );
+  });
+
+  const selectAllCheckbox = screen.getAllByLabelText('Select all')[0];
+  await userEvent.click(selectAllCheckbox);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+      `${mockDashboards.length} Selected`,
+    );
+  });
+
+  const bulkActions = screen.getAllByTestId('bulk-select-action');
+  const exportButton = bulkActions.find(btn => btn.textContent === 'Export');
+  expect(exportButton).toBeInTheDocument();
+  await userEvent.click(exportButton!);
+
+  await waitFor(() => {
+    expect(mockHandleResourceExport).toHaveBeenCalledWith(
+      'dashboard',
+      mockDashboards.map(d => d.id),
+      expect.any(Function),
+    );
+  });
+});
+
+test('supports bulk delete of selected dashboards', async () => {
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(mockDashboards[0].dashboard_title),
+    ).toBeInTheDocument();
+  });
+
+  const bulkSelectButton = screen.getByTestId('bulk-select');
+  await userEvent.click(bulkSelectButton);
+
+  await waitFor(() => {
+    expect(screen.getAllByRole('checkbox')).toHaveLength(
+      mockDashboards.length + 1,
+    );
+  });
+
+  const selectAllCheckbox = screen.getAllByLabelText('Select all')[0];
+  await userEvent.click(selectAllCheckbox);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+      `${mockDashboards.length} Selected`,
+    );
+  });
+
+  const bulkActions = screen.getAllByTestId('bulk-select-action');
+  const deleteButton = bulkActions.find(btn => btn.textContent === 'Delete');
+  expect(deleteButton).toBeInTheDocument();
+  await userEvent.click(deleteButton!);
+
+  await waitFor(() => {
+    const deleteModal = screen.getByRole('dialog');
+    expect(deleteModal).toBeInTheDocument();
+    expect(deleteModal).toHaveTextContent(/delete/i);
+    expect(deleteModal).toHaveTextContent(/selected dashboards/i);
+  });
+
+  // Type DELETE in the confirmation input
+  const deleteInput = screen.getByTestId('delete-modal-input');
+  await userEvent.type(deleteInput, 'DELETE');
+
+  // Mock the bulk DELETE endpoint
+  fetchMock.delete('glob:*/api/v1/dashboard/?*', {
+    message: 'Dashboards deleted',
+  });
+
+  // Click confirm button
+  const confirmButton = screen.getByTestId('modal-confirm-button');
+  fireEvent.click(confirmButton);
+
+  // Verify bulk delete API was called
+  await waitFor(() => {
+    const deleteCalls = fetchMock.callHistory.calls(/api\/v1\/dashboard\/\?/, {
+      method: 'DELETE',
+    });
+    expect(deleteCalls).toHaveLength(1);
+  });
+});
+
+test('displays certified badge only for certified dashboards', async () => {
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(mockDashboards[0].dashboard_title),
+    ).toBeInTheDocument();
+  });
+
+  const table = screen.getByTestId('listview-table');
+
+  // mockDashboards[0] is certified (certified_by: 'Data Team')
+  const certifiedRow = within(table)
+    .getByText(mockDashboards[0].dashboard_title)
+    .closest('[data-test="table-row"]') as HTMLElement;
+  expect(within(certifiedRow).getByLabelText('certified')).toBeInTheDocument();
+
+  // mockDashboards[1] is not certified (certified_by: null)
+  const uncertifiedRow = within(table)
+    .getByText(mockDashboards[1].dashboard_title)
+    .closest('[data-test="table-row"]') as HTMLElement;
+  expect(
+    within(uncertifiedRow).queryByLabelText('certified'),
+  ).not.toBeInTheDocument();
+});
+
+test('exits bulk select on button toggle', async () => {
+  renderDashboardList(mockUser);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
+  });
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(mockDashboards[0].dashboard_title),
+    ).toBeInTheDocument();
+  });
+
+  const bulkSelectButton = screen.getByTestId('bulk-select');
+  await userEvent.click(bulkSelectButton);
+
+  await waitFor(() => {
+    expect(screen.getAllByRole('checkbox')).toHaveLength(
+      mockDashboards.length + 1,
+    );
+  });
+
+  const table = screen.getByTestId('listview-table');
+  const dataRows = within(table).getAllByTestId('table-row');
+  const firstRowCheckbox = within(dataRows[0]).getByRole('checkbox');
+  await userEvent.click(firstRowCheckbox);
+
+  await waitFor(() => {
+    expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
+      '1 Selected',
+    );
+  });
+
+  await userEvent.click(bulkSelectButton);
+
+  await waitFor(() => {
+    expect(screen.queryAllByRole('checkbox')).toHaveLength(0);
+    expect(screen.queryByTestId('bulk-select-copy')).not.toBeInTheDocument();
+  });
+});
diff --git 
a/superset-frontend/src/pages/DashboardList/DashboardList.permissions.test.tsx 
b/superset-frontend/src/pages/DashboardList/DashboardList.permissions.test.tsx
new file mode 100644
index 0000000000..68d1229d97
--- /dev/null
+++ 
b/superset-frontend/src/pages/DashboardList/DashboardList.permissions.test.tsx
@@ -0,0 +1,340 @@
+/**
+ * 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 fetchMock from 'fetch-mock';
+import { render, screen, waitFor } 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 { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
+import { isFeatureEnabled } from '@superset-ui/core';
+import DashboardListComponent from 'src/pages/DashboardList';
+import {
+  API_ENDPOINTS,
+  mockDashboards,
+  setupMocks,
+} from './DashboardList.testHelpers';
+
+// Cast to accept partial mock props in tests
+const DashboardList = DashboardListComponent as unknown as React.FC<
+  Record<string, any>
+>;
+
+jest.setTimeout(30000);
+
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  isFeatureEnabled: jest.fn(),
+}));
+
+jest.mock('src/utils/export', () => ({
+  __esModule: true,
+  default: jest.fn(),
+}));
+
+// Permission configurations
+const PERMISSIONS = {
+  ADMIN: [
+    ['can_write', 'Dashboard'],
+    ['can_export', 'Dashboard'],
+    ['can_read', 'Tag'],
+  ],
+  READ_ONLY: [],
+  EXPORT_ONLY: [['can_export', 'Dashboard']],
+  WRITE_ONLY: [['can_write', 'Dashboard']],
+};
+
+const createMockUser = (overrides = {}) => ({
+  userId: 1,
+  firstName: 'Test',
+  lastName: 'User',
+  roles: {
+    Admin: [
+      ['can_write', 'Dashboard'],
+      ['can_export', 'Dashboard'],
+    ],
+  },
+  ...overrides,
+});
+
+const createMockStore = (initialState: any = {}) =>
+  configureStore({
+    reducer: {
+      user: (state = initialState.user || {}, action: any) => state,
+      common: (state = initialState.common || {}, action: any) => state,
+      dashboards: (state = initialState.dashboards || {}, action: any) => 
state,
+    },
+    preloadedState: initialState,
+    middleware: getDefaultMiddleware =>
+      getDefaultMiddleware({
+        serializableCheck: false,
+        immutableCheck: false,
+      }),
+  });
+
+const createStoreStateWithPermissions = (
+  permissions = PERMISSIONS.ADMIN,
+  userId: number | undefined = 1,
+) => ({
+  user: userId
+    ? {
+        ...createMockUser({ userId }),
+        roles: { TestRole: permissions },
+      }
+    : {},
+  common: {
+    conf: {
+      SUPERSET_WEBSERVER_TIMEOUT: 60000,
+    },
+  },
+  dashboards: {
+    dashboardList: mockDashboards,
+  },
+});
+
+const renderDashboardListWithPermissions = (
+  props = {},
+  storeState = {},
+  user = createMockUser(),
+) => {
+  const storeStateWithUser = {
+    ...createStoreStateWithPermissions(),
+    user,
+    ...storeState,
+  };
+
+  const store = createMockStore(storeStateWithUser);
+
+  return render(
+    <Provider store={store}>
+      <MemoryRouter>
+        <QueryParamProvider adapter={ReactRouter5Adapter}>
+          <DashboardList user={user} {...props} />
+        </QueryParamProvider>
+      </MemoryRouter>
+    </Provider>,
+  );
+};
+
+const renderWithPermissions = async (
+  permissions = PERMISSIONS.ADMIN,
+  userId: number | undefined = 1,
+  featureFlags: { tagging?: boolean; cardView?: boolean } = {},
+) => {
+  (
+    isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
+  ).mockImplementation((feature: string) => {
+    if (feature === 'TAGGING_SYSTEM') return featureFlags.tagging === true;
+    if (feature === 'LISTVIEWS_DEFAULT_CARD_VIEW')
+      return featureFlags.cardView === true;
+    return false;
+  });
+
+  // Convert role permissions to API permissions
+  setupMocks({
+    [API_ENDPOINTS.DASHBOARDS_INFO]: permissions.map(perm => perm[0]),
+  });
+
+  const storeState = createStoreStateWithPermissions(permissions, userId);
+
+  const userProps = userId
+    ? {
+        user: {
+          ...createMockUser({ userId }),
+          roles: { TestRole: permissions },
+        },
+      }
+    : { user: { userId: undefined } };
+
+  const result = renderDashboardListWithPermissions(userProps, storeState);
+  await waitFor(() => {
+    expect(screen.getByTestId('dashboard-list-view')).toBeInTheDocument();
+  });
+  return result;
+};
+
+// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
+describe('DashboardList - Permission-based UI Tests', () => {
+  beforeEach(() => {
+    fetchMock.clearHistory().removeRoutes();
+    (
+      isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
+    ).mockReset();
+  });
+
+  test('shows all UI elements for admin users with full permissions', async () 
=> {
+    await renderWithPermissions(PERMISSIONS.ADMIN);
+
+    await screen.findByTestId('dashboard-list-view');
+
+    // Verify admin controls are visible
+    expect(
+      screen.getByRole('button', { name: /dashboard/i }),
+    ).toBeInTheDocument();
+    expect(screen.getByTestId('import-button')).toBeInTheDocument();
+    expect(screen.getByTestId('bulk-select')).toBeInTheDocument();
+
+    // Verify Actions column is visible
+    expect(screen.getByTitle('Actions')).toBeInTheDocument();
+
+    // Verify favorite stars are rendered
+    const favoriteStars = screen.getAllByTestId('fave-unfave-icon');
+    expect(favoriteStars).toHaveLength(mockDashboards.length);
+  });
+
+  test('renders basic UI for anonymous users without permissions', async () => 
{
+    await renderWithPermissions(PERMISSIONS.READ_ONLY, undefined);
+    await screen.findByTestId('dashboard-list-view');
+
+    // Verify basic structure renders
+    expect(screen.getByTestId('dashboard-list-view')).toBeInTheDocument();
+    expect(screen.getByText('Dashboards')).toBeInTheDocument();
+
+    // Verify view toggles are available (not permission-gated)
+    expect(screen.getByRole('img', { name: 'appstore' })).toBeInTheDocument();
+    expect(
+      screen.getByRole('img', { name: 'unordered-list' }),
+    ).toBeInTheDocument();
+
+    // Verify permission-gated elements are hidden
+    expect(
+      screen.queryByRole('button', { name: /dashboard/i }),
+    ).not.toBeInTheDocument();
+    expect(screen.queryByTestId('import-button')).not.toBeInTheDocument();
+  });
+
+  test('shows Actions column for users with admin permissions', async () => {
+    await renderWithPermissions(PERMISSIONS.ADMIN);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(screen.getByTitle('Actions')).toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(
+        screen.getByText(mockDashboards[0].dashboard_title),
+      ).toBeInTheDocument();
+    });
+  });
+
+  test('hides Actions column for users with read-only permissions', async () 
=> {
+    await renderWithPermissions(PERMISSIONS.READ_ONLY);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(screen.queryByTitle('Actions')).not.toBeInTheDocument();
+    expect(screen.queryAllByLabelText('more')).toHaveLength(0);
+  });
+
+  test('shows Actions column for users with export-only permissions', async () 
=> {
+    // DashboardList shows Actions column when canExport is true
+    await renderWithPermissions(PERMISSIONS.EXPORT_ONLY);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(screen.getByTitle('Actions')).toBeInTheDocument();
+  });
+
+  test('shows Actions column for users with write-only permissions', async () 
=> {
+    await renderWithPermissions(PERMISSIONS.WRITE_ONLY);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(screen.getByTitle('Actions')).toBeInTheDocument();
+  });
+
+  test('shows Tags column when TAGGING_SYSTEM feature flag is enabled', async 
() => {
+    await renderWithPermissions(PERMISSIONS.ADMIN, 1, { tagging: true });
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(screen.getByTitle('Tags')).toBeInTheDocument();
+  });
+
+  test('hides Tags column when TAGGING_SYSTEM feature flag is disabled', async 
() => {
+    await renderWithPermissions(PERMISSIONS.ADMIN, 1, { tagging: false });
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(screen.queryByText('Tags')).not.toBeInTheDocument();
+  });
+
+  test('shows bulk select button for users with admin permissions', async () 
=> {
+    await renderWithPermissions(PERMISSIONS.ADMIN);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(screen.getByTestId('bulk-select')).toBeInTheDocument();
+  });
+
+  test('shows bulk select button for users with export-only permissions', 
async () => {
+    await renderWithPermissions(PERMISSIONS.EXPORT_ONLY);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(screen.getByTestId('bulk-select')).toBeInTheDocument();
+  });
+
+  test('shows bulk select button for users with write-only permissions', async 
() => {
+    await renderWithPermissions(PERMISSIONS.WRITE_ONLY);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(screen.getByTestId('bulk-select')).toBeInTheDocument();
+  });
+
+  test('hides bulk select button for users with read-only permissions', async 
() => {
+    await renderWithPermissions(PERMISSIONS.READ_ONLY);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(screen.queryByTestId('bulk-select')).not.toBeInTheDocument();
+  });
+
+  test('shows Create and Import buttons for users with write permissions', 
async () => {
+    await renderWithPermissions(PERMISSIONS.WRITE_ONLY);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(
+      screen.getByRole('button', { name: /dashboard/i }),
+    ).toBeInTheDocument();
+    expect(screen.getByTestId('import-button')).toBeInTheDocument();
+  });
+
+  test('hides Create and Import buttons for users with read-only permissions', 
async () => {
+    await renderWithPermissions(PERMISSIONS.READ_ONLY);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(
+      screen.queryByRole('button', { name: /dashboard/i }),
+    ).not.toBeInTheDocument();
+    expect(screen.queryByTestId('import-button')).not.toBeInTheDocument();
+  });
+
+  test('hides Create and Import buttons for users with export-only 
permissions', async () => {
+    await renderWithPermissions(PERMISSIONS.EXPORT_ONLY);
+    await screen.findByTestId('dashboard-list-view');
+
+    expect(
+      screen.queryByRole('button', { name: /dashboard/i }),
+    ).not.toBeInTheDocument();
+    expect(screen.queryByTestId('import-button')).not.toBeInTheDocument();
+  });
+
+  test('renders favorite stars even for anonymous user', async () => {
+    // Current behavior: Component renders favorites regardless of userId
+    // (matches ChartList behavior — antd hidden column + Cell guard
+    // do not prevent rendering in JSDOM)
+    await renderWithPermissions(PERMISSIONS.READ_ONLY, undefined);
+    await screen.findByTestId('dashboard-list-view');
+
+    const favoriteStars = screen.getAllByTestId('fave-unfave-icon');
+    expect(favoriteStars).toHaveLength(mockDashboards.length);
+  });
+});
diff --git a/superset-frontend/src/pages/DashboardList/DashboardList.test.tsx 
b/superset-frontend/src/pages/DashboardList/DashboardList.test.tsx
index a5545b499b..2f4b8f9dc5 100644
--- a/superset-frontend/src/pages/DashboardList/DashboardList.test.tsx
+++ b/superset-frontend/src/pages/DashboardList/DashboardList.test.tsx
@@ -16,233 +16,290 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React from 'react';
-import { MemoryRouter } from 'react-router-dom';
 import fetchMock from 'fetch-mock';
 import { isFeatureEnabled } from '@superset-ui/core';
 import {
-  render,
   screen,
+  selectOption,
   waitFor,
   fireEvent,
 } from 'spec/helpers/testing-library';
-import { QueryParamProvider } from 'use-query-params';
-import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
-
-import DashboardListComponent from 'src/pages/DashboardList';
-
-// Cast to accept partial mock props in tests
-const DashboardList = DashboardListComponent as unknown as React.FC<
-  Record<string, any>
->;
+import {
+  mockDashboards,
+  mockAdminUser,
+  setupMocks,
+  renderDashboardList,
+  API_ENDPOINTS,
+  getLatestDashboardApiCall,
+} from './DashboardList.testHelpers';
 
-const dashboardsInfoEndpoint = 'glob:*/api/v1/dashboard/_info*';
-const dashboardOwnersEndpoint = 'glob:*/api/v1/dashboard/related/owners*';
-const dashboardCreatedByEndpoint =
-  'glob:*/api/v1/dashboard/related/created_by*';
-const dashboardFavoriteStatusEndpoint =
-  'glob:*/api/v1/dashboard/favorite_status*';
-const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*';
-const dashboardEndpoint = 'glob:*/api/v1/dashboard/*';
+jest.setTimeout(30000);
 
 jest.mock('@superset-ui/core', () => ({
   ...jest.requireActual('@superset-ui/core'),
   isFeatureEnabled: jest.fn(),
 }));
 
-const mockDashboards = Array.from({ length: 3 }, (_, i) => ({
-  id: i,
-  url: 'url',
-  dashboard_title: `title ${i}`,
-  changed_by_name: 'user',
-  changed_by_fk: 1,
-  published: true,
-  changed_on_utc: new Date().toISOString(),
-  changed_on_delta_humanized: '5 minutes ago',
-  owners: [{ id: 1, first_name: 'admin', last_name: 'admin_user' }],
-  roles: [{ id: 1, name: 'adminUser' }],
-  thumbnail_url: '/thumbnail',
+jest.mock('src/utils/export', () => ({
+  __esModule: true,
+  default: jest.fn(),
 }));
 
-const mockUser = {
-  userId: 1,
-};
+const mockIsFeatureEnabled = isFeatureEnabled as jest.MockedFunction<
+  typeof isFeatureEnabled
+>;
 
-fetchMock.get(dashboardsInfoEndpoint, {
-  permissions: ['can_read', 'can_write'],
+beforeEach(() => {
+  setupMocks();
+  mockIsFeatureEnabled.mockImplementation(
+    (feature: string) => feature === 'LISTVIEWS_DEFAULT_CARD_VIEW',
+  );
 });
-fetchMock.get(dashboardOwnersEndpoint, {
-  result: [],
+
+afterEach(() => {
+  fetchMock.clearHistory().removeRoutes();
+  mockIsFeatureEnabled.mockReset();
 });
-fetchMock.get(dashboardCreatedByEndpoint, {
-  result: [],
+
+test('renders', async () => {
+  renderDashboardList(mockAdminUser);
+  expect(await screen.findByText('Dashboards')).toBeInTheDocument();
 });
-fetchMock.get(dashboardFavoriteStatusEndpoint, {
-  result: [],
+
+test('renders a ListView', async () => {
+  renderDashboardList(mockAdminUser);
+  expect(await screen.findByTestId('dashboard-list-view')).toBeInTheDocument();
 });
-fetchMock.get(dashboardsEndpoint, {
-  result: mockDashboards,
-  dashboard_count: 3,
+
+test('fetches info', async () => {
+  renderDashboardList(mockAdminUser);
+  await waitFor(() => {
+    const calls = fetchMock.callHistory.calls(/dashboard\/_info/);
+    expect(calls).toHaveLength(1);
+  });
 });
-fetchMock.get(dashboardEndpoint, {
-  result: mockDashboards[0],
+
+test('fetches data', async () => {
+  renderDashboardList(mockAdminUser);
+  await waitFor(() => {
+    const calls = fetchMock.callHistory.calls(/dashboard\/\?q/);
+    expect(calls).toHaveLength(1);
+  });
+
+  const calls = fetchMock.callHistory.calls(/dashboard\/\?q/);
+  expect(calls[0].url).toMatchInlineSnapshot(
+    
`"http://localhost/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25,select_columns:!(id,dashboard_title,published,url,slug,changed_by,changed_by.id,changed_by.first_name,changed_by.last_name,changed_on_delta_humanized,owners,owners.id,owners.first_name,owners.last_name,tags.id,tags.name,tags.type,status,certified_by,certification_details,changed_on))"`,
+  );
 });
 
-global.URL.createObjectURL = jest.fn();
-fetchMock.get('/thumbnail', { body: new Blob(), sendAsJson: false });
-
-// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
-describe('DashboardList', () => {
-  const renderDashboardList = (props = {}, userProp = mockUser) =>
-    render(
-      <MemoryRouter>
-        <QueryParamProvider adapter={ReactRouter5Adapter}>
-          <DashboardList {...props} user={userProp} />
-        </QueryParamProvider>
-      </MemoryRouter>,
-      { useRedux: true },
-    );
+test('switches between card and table view', async () => {
+  renderDashboardList(mockAdminUser);
 
-  beforeEach(() => {
-    (isFeatureEnabled as jest.Mock).mockImplementation(
-      (feature: string) => feature === 'LISTVIEWS_DEFAULT_CARD_VIEW',
-    );
-    fetchMock.clearHistory();
-  });
+  // Wait for the list to load
+  await screen.findByTestId('dashboard-list-view');
 
-  afterEach(() => {
-    (isFeatureEnabled as jest.Mock).mockRestore();
-  });
+  // Initially in card view (no table)
+  expect(screen.queryByTestId('listview-table')).not.toBeInTheDocument();
 
-  test('renders', async () => {
-    renderDashboardList();
-    expect(await screen.findByText('Dashboards')).toBeInTheDocument();
-  });
+  // Switch to table view via the list icon
+  const listViewIcon = screen.getByRole('img', { name: 'unordered-list' });
+  const listViewButton = listViewIcon.closest('[role="button"]')!;
+  fireEvent.click(listViewButton);
 
-  test('renders a ListView', async () => {
-    renderDashboardList();
-    expect(
-      await screen.findByTestId('dashboard-list-view'),
-    ).toBeInTheDocument();
+  await waitFor(() => {
+    expect(screen.getByTestId('listview-table')).toBeInTheDocument();
   });
 
-  test('fetches info', async () => {
-    renderDashboardList();
-    await waitFor(() => {
-      const calls = fetchMock.callHistory.calls(/dashboard\/_info/);
-      expect(calls).toHaveLength(1);
-    });
+  // Switch back to card view
+  const cardViewIcon = screen.getByRole('img', { name: 'appstore' });
+  const cardViewButton = cardViewIcon.closest('[role="button"]')!;
+  fireEvent.click(cardViewButton);
+
+  await waitFor(() => {
+    expect(screen.queryByTestId('listview-table')).not.toBeInTheDocument();
   });
+});
 
-  test('fetches data', async () => {
-    renderDashboardList();
-    await waitFor(() => {
-      const calls = fetchMock.callHistory.calls(/dashboard\/\?q/);
-      expect(calls).toHaveLength(1);
-    });
+test('shows edit modal', async () => {
+  renderDashboardList(mockAdminUser);
 
-    const calls = fetchMock.callHistory.calls(/dashboard\/\?q/);
-    expect(calls[0].url).toMatchInlineSnapshot(
-      
`"http://localhost/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25,select_columns:!(id,dashboard_title,published,url,slug,changed_by,changed_by.id,changed_by.first_name,changed_by.last_name,changed_on_delta_humanized,owners,owners.id,owners.first_name,owners.last_name,tags.id,tags.name,tags.type,status,certified_by,certification_details,changed_on))"`,
-    );
+  // Wait for data to load
+  await screen.findByText(mockDashboards[0].dashboard_title);
+
+  // Find and click the first more options button
+  const moreIcons = await screen.findAllByRole('img', {
+    name: 'more',
   });
+  fireEvent.click(moreIcons[0]);
 
-  test('switches between card and table view', async () => {
-    renderDashboardList();
+  // Click edit from the dropdown
+  const editButton = await screen.findByTestId(
+    'dashboard-card-option-edit-button',
+  );
+  fireEvent.click(editButton);
 
-    // Wait for the list to load
-    await screen.findByTestId('dashboard-list-view');
+  // Check for modal
+  expect(await screen.findByRole('dialog')).toBeInTheDocument();
+});
 
-    // Initially in card view
-    const cardViewIcon = screen.getByRole('img', { name: 'appstore' });
-    expect(cardViewIcon).toBeInTheDocument();
+test('shows delete confirmation', async () => {
+  renderDashboardList(mockAdminUser);
 
-    // Switch to table view
-    const listViewIcon = screen.getByRole('img', { name: 'appstore' });
-    const listViewButton = listViewIcon.closest('[role="button"]')!;
-    fireEvent.click(listViewButton);
+  // Wait for data to load
+  await screen.findByText(mockDashboards[0].dashboard_title);
 
-    // Switch back to card view
-    const cardViewButton = cardViewIcon.closest('[role="button"]')!;
-    fireEvent.click(cardViewButton);
+  // Find and click the first more options button
+  const moreIcons = await screen.findAllByRole('img', {
+    name: 'more',
   });
+  fireEvent.click(moreIcons[0]);
+
+  // Click delete from the dropdown
+  const deleteButton = await screen.findByTestId(
+    'dashboard-card-option-delete-button',
+  );
+  fireEvent.click(deleteButton);
+
+  // Check for confirmation dialog
+  expect(
+    await screen.findByText(/Are you sure you want to delete/i),
+  ).toBeInTheDocument();
+});
 
-  test('shows edit modal', async () => {
-    renderDashboardList();
-
-    // Wait for data to load
-    await screen.findByText('title 0');
+test('renders an "Import Dashboard" tooltip', async () => {
+  renderDashboardList(mockAdminUser);
 
-    // Find and click the first more options button
-    const moreIcons = await screen.findAllByRole('img', {
-      name: 'more',
-    });
-    fireEvent.click(moreIcons[0]);
+  const importButton = await screen.findByTestId('import-button');
+  fireEvent.mouseOver(importButton);
 
-    // Click edit from the dropdown
-    const editButton = await screen.findByTestId(
-      'dashboard-card-option-edit-button',
-    );
-    fireEvent.click(editButton);
+  expect(
+    await screen.findByRole('tooltip', {
+      name: 'Import dashboards',
+    }),
+  ).toBeInTheDocument();
+});
 
-    // Check for modal
-    expect(await screen.findByRole('dialog')).toBeInTheDocument();
-  });
+test('renders all standard filters', async () => {
+  renderDashboardList(mockAdminUser);
+  await screen.findByTestId('dashboard-list-view');
 
-  test('shows delete confirmation', async () => {
-    renderDashboardList();
+  // Verify filter labels exist
+  expect(screen.getByText('Owner')).toBeInTheDocument();
+  expect(screen.getByText('Status')).toBeInTheDocument();
+  expect(screen.getByText('Modified by')).toBeInTheDocument();
+  expect(screen.getByText('Certified')).toBeInTheDocument();
+});
 
-    // Wait for data to load
-    await screen.findByText('title 0');
+test('selecting Status filter encodes published=true in API call', async () => 
{
+  renderDashboardList(mockAdminUser);
+  await screen.findByTestId('dashboard-list-view');
 
-    // Find and click the first more options button
-    const moreIcons = await screen.findAllByRole('img', {
-      name: 'more',
-    });
-    fireEvent.click(moreIcons[0]);
+  await waitFor(() => {
+    expect(
+      screen.getByText(mockDashboards[0].dashboard_title),
+    ).toBeInTheDocument();
+  });
 
-    // Click delete from the dropdown
-    const deleteButton = await screen.findByTestId(
-      'dashboard-card-option-delete-button',
+  await selectOption('Published', 'Status');
+
+  await waitFor(() => {
+    const latest = getLatestDashboardApiCall();
+    expect(latest).not.toBeNull();
+    expect(latest!.query!.filters).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({
+          col: 'published',
+          opr: 'eq',
+          value: true,
+        }),
+      ]),
     );
-    fireEvent.click(deleteButton);
+  });
+});
 
-    // Check for confirmation dialog
+test('selecting Owner filter encodes rel_m_m owner in API call', async () => {
+  // Replace the owners route to return a selectable option
+  fetchMock.removeRoutes({
+    names: [API_ENDPOINTS.DASHBOARD_RELATED_OWNERS, API_ENDPOINTS.CATCH_ALL],
+  });
+  fetchMock.get(
+    API_ENDPOINTS.DASHBOARD_RELATED_OWNERS,
+    { result: [{ value: 1, text: 'Admin User' }], count: 1 },
+    { name: API_ENDPOINTS.DASHBOARD_RELATED_OWNERS },
+  );
+  fetchMock.get(API_ENDPOINTS.CATCH_ALL, (callLog: any) => {
+    const reqUrl =
+      typeof callLog === 'string' ? callLog : callLog?.url || callLog;
+    throw new Error(`[fetchMock catch-all] Unmatched GET: ${reqUrl}`);
+  });
+
+  renderDashboardList(mockAdminUser);
+  await screen.findByTestId('dashboard-list-view');
+
+  await waitFor(() => {
     expect(
-      await screen.findByText(/Are you sure you want to delete/i),
+      screen.getByText(mockDashboards[0].dashboard_title),
     ).toBeInTheDocument();
   });
 
-  test('renders an "Import Dashboard" tooltip', async () => {
-    renderDashboardList();
+  await selectOption('Admin User', 'Owner');
+
+  await waitFor(() => {
+    const latest = getLatestDashboardApiCall();
+    expect(latest).not.toBeNull();
+    expect(latest!.query!.filters).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({
+          col: 'owners',
+          opr: 'rel_m_m',
+          value: 1,
+        }),
+      ]),
+    );
+  });
+});
+
+test('selecting Modified by filter encodes rel_o_m changed_by in API call', 
async () => {
+  // Replace the changed_by route to return a selectable option
+  fetchMock.removeRoutes({
+    names: [
+      API_ENDPOINTS.DASHBOARD_RELATED_CHANGED_BY,
+      API_ENDPOINTS.CATCH_ALL,
+    ],
+  });
+  fetchMock.get(
+    API_ENDPOINTS.DASHBOARD_RELATED_CHANGED_BY,
+    { result: [{ value: 1, text: 'Admin User' }], count: 1 },
+    { name: API_ENDPOINTS.DASHBOARD_RELATED_CHANGED_BY },
+  );
+  fetchMock.get(API_ENDPOINTS.CATCH_ALL, (callLog: any) => {
+    const reqUrl =
+      typeof callLog === 'string' ? callLog : callLog?.url || callLog;
+    throw new Error(`[fetchMock catch-all] Unmatched GET: ${reqUrl}`);
+  });
 
-    const importButton = await screen.findByTestId('import-button');
-    fireEvent.mouseOver(importButton);
+  renderDashboardList(mockAdminUser);
+  await screen.findByTestId('dashboard-list-view');
 
+  await waitFor(() => {
     expect(
-      await screen.findByRole('tooltip', {
-        name: 'Import dashboards',
-      }),
+      screen.getByText(mockDashboards[0].dashboard_title),
     ).toBeInTheDocument();
   });
-});
 
-// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
-describe('DashboardList - anonymous view', () => {
-  test('does not render favorite stars for anonymous user', async () => {
-    render(
-      <MemoryRouter>
-        <QueryParamProvider adapter={ReactRouter5Adapter}>
-          <DashboardList user={{}} />
-        </QueryParamProvider>
-      </MemoryRouter>,
-      { useRedux: true },
+  await selectOption('Admin User', 'Modified by');
+
+  await waitFor(() => {
+    const latest = getLatestDashboardApiCall();
+    expect(latest).not.toBeNull();
+    expect(latest!.query!.filters).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({
+          col: 'changed_by',
+          opr: 'rel_o_m',
+          value: 1,
+        }),
+      ]),
     );
-
-    await waitFor(() => {
-      expect(
-        screen.queryByRole('img', { name: /favorite/i }),
-      ).not.toBeInTheDocument();
-    });
   });
 });
diff --git 
a/superset-frontend/src/pages/DashboardList/DashboardList.testHelpers.tsx 
b/superset-frontend/src/pages/DashboardList/DashboardList.testHelpers.tsx
new file mode 100644
index 0000000000..b5cc1e4ea2
--- /dev/null
+++ b/superset-frontend/src/pages/DashboardList/DashboardList.testHelpers.tsx
@@ -0,0 +1,360 @@
+/**
+ * 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 rison from 'rison';
+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 { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
+import DashboardListComponent from 'src/pages/DashboardList';
+import handleResourceExport from 'src/utils/export';
+
+// Cast to accept partial mock props in tests
+const DashboardList = DashboardListComponent as unknown as React.FC<
+  Record<string, any>
+>;
+
+export const mockHandleResourceExport =
+  handleResourceExport as jest.MockedFunction<typeof handleResourceExport>;
+
+export const mockDashboards = [
+  {
+    id: 1,
+    url: '/superset/dashboard/1/',
+    dashboard_title: 'Sales Dashboard',
+    published: true,
+    changed_by_name: 'admin',
+    changed_by_fk: 1,
+    changed_by: {
+      first_name: 'Admin',
+      last_name: 'User',
+      id: 1,
+    },
+    changed_on_utc: new Date().toISOString(),
+    changed_on_delta_humanized: '1 day ago',
+    owners: [{ id: 1, first_name: 'Admin', last_name: 'User' }],
+    roles: [{ id: 1, name: 'Admin' }],
+    tags: [{ id: 1, name: 'production', type: 'TagTypes.custom' }],
+    thumbnail_url: '/thumbnail',
+    certified_by: 'Data Team',
+    certification_details: 'Approved for production use',
+    status: 'published',
+  },
+  {
+    id: 2,
+    url: '/superset/dashboard/2/',
+    dashboard_title: 'Analytics Dashboard',
+    published: false,
+    changed_by_name: 'analyst',
+    changed_by_fk: 2,
+    changed_by: {
+      first_name: 'Data',
+      last_name: 'Analyst',
+      id: 2,
+    },
+    changed_on_utc: new Date().toISOString(),
+    changed_on_delta_humanized: '2 days ago',
+    owners: [
+      { id: 1, first_name: 'Admin', last_name: 'User' },
+      { id: 2, first_name: 'Data', last_name: 'Analyst' },
+    ],
+    roles: [],
+    tags: [],
+    thumbnail_url: '/thumbnail',
+    certified_by: null,
+    certification_details: null,
+    status: 'draft',
+  },
+  {
+    id: 3,
+    url: '/superset/dashboard/3/',
+    dashboard_title: 'Executive Overview',
+    published: true,
+    changed_by_name: 'admin',
+    changed_by_fk: 1,
+    changed_by: {
+      first_name: 'Admin',
+      last_name: 'User',
+      id: 1,
+    },
+    changed_on_utc: new Date().toISOString(),
+    changed_on_delta_humanized: '3 days ago',
+    owners: [],
+    roles: [{ id: 2, name: 'Alpha' }],
+    tags: [
+      { id: 2, name: 'executive', type: 'TagTypes.custom' },
+      { id: 3, name: 'quarterly', type: 'TagTypes.custom' },
+    ],
+    thumbnail_url: '/thumbnail',
+    certified_by: 'QA Team',
+    certification_details: 'Verified for executive use',
+    status: 'published',
+  },
+  {
+    id: 4,
+    url: '/superset/dashboard/4/',
+    dashboard_title: 'Marketing Metrics',
+    published: false,
+    changed_by_name: 'marketing',
+    changed_by_fk: 3,
+    changed_by: {
+      first_name: 'Marketing',
+      last_name: 'Lead',
+      id: 3,
+    },
+    changed_on_utc: new Date().toISOString(),
+    changed_on_delta_humanized: '5 days ago',
+    owners: [{ id: 3, first_name: 'Marketing', last_name: 'Lead' }],
+    roles: [],
+    tags: [],
+    thumbnail_url: '/thumbnail',
+    certified_by: null,
+    certification_details: null,
+    status: 'draft',
+  },
+  {
+    id: 5,
+    url: '/superset/dashboard/5/',
+    dashboard_title: 'Ops Monitor',
+    published: true,
+    changed_by_name: 'ops',
+    changed_by_fk: 4,
+    changed_by: {
+      first_name: 'Ops',
+      last_name: 'Engineer',
+      id: 4,
+    },
+    changed_on_utc: new Date().toISOString(),
+    changed_on_delta_humanized: '1 week ago',
+    owners: [
+      { id: 4, first_name: 'Ops', last_name: 'Engineer' },
+      { id: 1, first_name: 'Admin', last_name: 'User' },
+    ],
+    roles: [],
+    tags: [{ id: 4, name: 'monitoring', type: 'TagTypes.custom' }],
+    thumbnail_url: '/thumbnail',
+    certified_by: null,
+    certification_details: null,
+    status: 'published',
+  },
+];
+
+// Mock users with various permission levels
+export const mockAdminUser = {
+  userId: 1,
+  firstName: 'Admin',
+  lastName: 'User',
+  roles: {
+    Admin: [
+      ['can_write', 'Dashboard'],
+      ['can_export', 'Dashboard'],
+      ['can_read', 'Tag'],
+    ],
+  },
+};
+
+export const mockReadOnlyUser = {
+  userId: 10,
+  firstName: 'Read',
+  lastName: 'Only',
+  roles: {
+    Gamma: [['can_read', 'Dashboard']],
+  },
+};
+
+export const mockExportOnlyUser = {
+  userId: 11,
+  firstName: 'Export',
+  lastName: 'User',
+  roles: {
+    Gamma: [
+      ['can_read', 'Dashboard'],
+      ['can_export', 'Dashboard'],
+    ],
+  },
+};
+
+// API endpoint constants
+export const API_ENDPOINTS = {
+  DASHBOARDS_INFO: 'glob:*/api/v1/dashboard/_info*',
+  DASHBOARDS: 'glob:*/api/v1/dashboard/?*',
+  DASHBOARD_GET: 'glob:*/api/v1/dashboard/*',
+  DASHBOARD_FAVORITE_STATUS: 'glob:*/api/v1/dashboard/favorite_status*',
+  DASHBOARD_RELATED_OWNERS: 'glob:*/api/v1/dashboard/related/owners*',
+  DASHBOARD_RELATED_CHANGED_BY: 'glob:*/api/v1/dashboard/related/changed_by*',
+  THUMBNAIL: '/thumbnail',
+  CATCH_ALL: 'glob:*',
+};
+
+interface StoreState {
+  user?: any;
+  common?: {
+    conf?: {
+      SUPERSET_WEBSERVER_TIMEOUT?: number;
+    };
+  };
+  dashboards?: {
+    dashboardList?: typeof mockDashboards;
+  };
+}
+
+export const createMockStore = (initialState: Partial<StoreState> = {}) =>
+  configureStore({
+    reducer: {
+      user: (state = initialState.user || {}) => state,
+      common: (state = initialState.common || {}) => state,
+      dashboards: (state = initialState.dashboards || {}) => state,
+    },
+    preloadedState: initialState,
+    middleware: getDefaultMiddleware =>
+      getDefaultMiddleware({
+        serializableCheck: false,
+        immutableCheck: false,
+      }),
+  });
+
+export const createDefaultStoreState = (user: any): StoreState => ({
+  user,
+  common: {
+    conf: {
+      SUPERSET_WEBSERVER_TIMEOUT: 60000,
+    },
+  },
+  dashboards: {
+    dashboardList: mockDashboards,
+  },
+});
+
+export const renderDashboardList = (
+  user: any,
+  props: Record<string, any> = {},
+  storeState: Partial<StoreState> = {},
+) => {
+  const defaultStoreState = createDefaultStoreState(user);
+  const storeStateWithUser = {
+    ...defaultStoreState,
+    user,
+    ...storeState,
+  };
+
+  const store = createMockStore(storeStateWithUser);
+
+  return render(
+    <Provider store={store}>
+      <MemoryRouter>
+        <QueryParamProvider adapter={ReactRouter5Adapter}>
+          <DashboardList user={user} {...props} />
+        </QueryParamProvider>
+      </MemoryRouter>
+    </Provider>,
+  );
+};
+
+/**
+ * Helper to wait for the DashboardList page to be ready
+ * Waits for the "Dashboards" heading to appear, indicating initial render is 
complete
+ */
+export const waitForDashboardsPageReady = async () => {
+  await screen.findByText('Dashboards');
+};
+
+export const setupMocks = (
+  payloadMap: Record<string, string[]> = {
+    [API_ENDPOINTS.DASHBOARDS_INFO]: ['can_read', 'can_write', 'can_export'],
+  },
+) => {
+  fetchMock.get(
+    API_ENDPOINTS.DASHBOARDS_INFO,
+    {
+      permissions: payloadMap[API_ENDPOINTS.DASHBOARDS_INFO],
+    },
+    { name: API_ENDPOINTS.DASHBOARDS_INFO },
+  );
+
+  fetchMock.get(
+    API_ENDPOINTS.DASHBOARDS,
+    {
+      result: mockDashboards,
+      dashboard_count: mockDashboards.length,
+    },
+    { name: API_ENDPOINTS.DASHBOARDS },
+  );
+
+  fetchMock.get(
+    API_ENDPOINTS.DASHBOARD_FAVORITE_STATUS,
+    { result: [] },
+    { name: API_ENDPOINTS.DASHBOARD_FAVORITE_STATUS },
+  );
+
+  fetchMock.get(
+    API_ENDPOINTS.DASHBOARD_RELATED_OWNERS,
+    { result: [], count: 0 },
+    { name: API_ENDPOINTS.DASHBOARD_RELATED_OWNERS },
+  );
+
+  fetchMock.get(
+    API_ENDPOINTS.DASHBOARD_RELATED_CHANGED_BY,
+    { result: [], count: 0 },
+    { name: API_ENDPOINTS.DASHBOARD_RELATED_CHANGED_BY },
+  );
+
+  global.URL.createObjectURL = jest.fn();
+  fetchMock.get(
+    API_ENDPOINTS.THUMBNAIL,
+    { body: new Blob(), sendAsJson: false },
+    { name: API_ENDPOINTS.THUMBNAIL },
+  );
+
+  fetchMock.get(
+    API_ENDPOINTS.CATCH_ALL,
+    (callLog: any) => {
+      const reqUrl =
+        typeof callLog === 'string' ? callLog : callLog?.url || callLog;
+      throw new Error(`[fetchMock catch-all] Unmatched GET: ${reqUrl}`);
+    },
+    { name: API_ENDPOINTS.CATCH_ALL },
+  );
+};
+
+/**
+ * Parse the rison-encoded `q` query parameter from a fetch-mock call URL.
+ * Returns the decoded object, or null if parsing fails.
+ */
+export const parseQueryFromUrl = (url: string): Record<string, any> | null => {
+  const match = url.match(/[?&]q=(.+?)(?:&|$)/);
+  if (!match) return null;
+  return rison.decode(decodeURIComponent(match[1]));
+};
+
+/**
+ * Get the last dashboard list API call from fetchMock history.
+ * Returns both the raw call and the parsed rison query.
+ */
+export const getLatestDashboardApiCall = () => {
+  const calls = fetchMock.callHistory.calls(/dashboard\/\?q/);
+  if (calls.length === 0) return null;
+  const lastCall = calls[calls.length - 1];
+  return {
+    call: lastCall,
+    query: parseQueryFromUrl(lastCall.url),
+  };
+};


Reply via email to