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

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

commit 92e9764a57b4a57964846fc6703df5f4666ee822
Author: Joe Li <[email protected]>
AuthorDate: Tue Dec 16 14:19:44 2025 -0800

    test(playwright): add edit dataset E2E test
    
    Add E2E test for editing a dataset description:
    - Click edit action to open DatasourceModal
    - Update description field
    - Save with confirmation dialog
    - Verify toast and persisted change
    
    Add supporting components:
    - EditDatasetModal using Modal base class and component abstractions
    - ConfirmDialog for reusable OK/Cancel confirmation dialogs
    - Textarea component for multi-line text input
    - Tabs component for tab navigation
    - clickEditAction method in DatasetListPage
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude Opus 4.5 <[email protected]>
---
 .../playwright/components/core/Tabs.ts             | 75 +++++++++++++++++++
 .../playwright/components/core/Textarea.ts         | 86 ++++++++++++++++++++++
 .../playwright/components/core/index.ts            |  2 +
 .../{core/index.ts => modals/ConfirmDialog.ts}     | 35 +++++++--
 .../components/modals/EditDatasetModal.ts          | 71 ++++++++++++++++++
 .../playwright/pages/DatasetListPage.ts            | 12 +++
 .../experimental/dataset/dataset-list.spec.ts      | 53 +++++++++++++
 7 files changed, 326 insertions(+), 8 deletions(-)

diff --git a/superset-frontend/playwright/components/core/Tabs.ts 
b/superset-frontend/playwright/components/core/Tabs.ts
new file mode 100644
index 0000000000..b2fe024911
--- /dev/null
+++ b/superset-frontend/playwright/components/core/Tabs.ts
@@ -0,0 +1,75 @@
+/**
+ * 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 { Locator, Page } from '@playwright/test';
+
+/**
+ * Tabs component for Ant Design tab navigation.
+ */
+export class Tabs {
+  readonly page: Page;
+  private readonly locator: Locator;
+
+  constructor(page: Page, locator?: Locator) {
+    this.page = page;
+    // Default to the tablist role if no specific locator provided
+    this.locator = locator ?? page.getByRole('tablist');
+  }
+
+  /**
+   * Gets the tablist element locator
+   */
+  get element(): Locator {
+    return this.locator;
+  }
+
+  /**
+   * Gets a tab by name
+   * @param tabName - The name/label of the tab
+   */
+  getTab(tabName: string): Locator {
+    return this.page.getByRole('tab', { name: tabName });
+  }
+
+  /**
+   * Clicks a tab by name
+   * @param tabName - The name/label of the tab to click
+   */
+  async clickTab(tabName: string): Promise<void> {
+    await this.getTab(tabName).click();
+  }
+
+  /**
+   * Gets the tab panel content for a given tab
+   * @param tabName - The name/label of the tab
+   */
+  getTabPanel(tabName: string): Locator {
+    return this.page.getByRole('tabpanel', { name: tabName });
+  }
+
+  /**
+   * Checks if a tab is selected
+   * @param tabName - The name/label of the tab
+   */
+  async isSelected(tabName: string): Promise<boolean> {
+    const tab = this.getTab(tabName);
+    const ariaSelected = await tab.getAttribute('aria-selected');
+    return ariaSelected === 'true';
+  }
+}
diff --git a/superset-frontend/playwright/components/core/Textarea.ts 
b/superset-frontend/playwright/components/core/Textarea.ts
new file mode 100644
index 0000000000..b33df32b8e
--- /dev/null
+++ b/superset-frontend/playwright/components/core/Textarea.ts
@@ -0,0 +1,86 @@
+/**
+ * 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 { Locator, Page } from '@playwright/test';
+
+/**
+ * Textarea component for multi-line text input interactions.
+ */
+export class Textarea {
+  readonly page: Page;
+  private readonly locator: Locator;
+
+  constructor(page: Page, locator: Locator) {
+    this.page = page;
+    this.locator = locator;
+  }
+
+  /**
+   * Creates a Textarea from a name attribute
+   * @param page - The Playwright page
+   * @param name - The name attribute value
+   */
+  static fromName(page: Page, name: string): Textarea {
+    const locator = page.locator(`textarea[name="${name}"]`);
+    return new Textarea(page, locator);
+  }
+
+  /**
+   * Gets the textarea element locator
+   */
+  get element(): Locator {
+    return this.locator;
+  }
+
+  /**
+   * Fills the textarea with text (clears existing content)
+   * @param text - The text to fill
+   */
+  async fill(text: string): Promise<void> {
+    await this.locator.fill(text);
+  }
+
+  /**
+   * Clears the textarea content
+   */
+  async clear(): Promise<void> {
+    await this.locator.clear();
+  }
+
+  /**
+   * Gets the current value of the textarea
+   */
+  async getValue(): Promise<string> {
+    return this.locator.inputValue();
+  }
+
+  /**
+   * Checks if the textarea is visible
+   */
+  async isVisible(): Promise<boolean> {
+    return this.locator.isVisible();
+  }
+
+  /**
+   * Checks if the textarea is enabled
+   */
+  async isEnabled(): Promise<boolean> {
+    return this.locator.isEnabled();
+  }
+}
diff --git a/superset-frontend/playwright/components/core/index.ts 
b/superset-frontend/playwright/components/core/index.ts
index de85d30906..27a55d20dd 100644
--- a/superset-frontend/playwright/components/core/index.ts
+++ b/superset-frontend/playwright/components/core/index.ts
@@ -25,3 +25,5 @@ export { Input } from './Input';
 export { Modal } from './Modal';
 export { Select } from './Select';
 export { Table } from './Table';
+export { Tabs } from './Tabs';
+export { Textarea } from './Textarea';
diff --git a/superset-frontend/playwright/components/core/index.ts 
b/superset-frontend/playwright/components/modals/ConfirmDialog.ts
similarity index 55%
copy from superset-frontend/playwright/components/core/index.ts
copy to superset-frontend/playwright/components/modals/ConfirmDialog.ts
index de85d30906..77834123e9 100644
--- a/superset-frontend/playwright/components/core/index.ts
+++ b/superset-frontend/playwright/components/modals/ConfirmDialog.ts
@@ -17,11 +17,30 @@
  * under the License.
  */
 
-// Core Playwright Components for Superset
-export { Button } from './Button';
-export { Checkbox } from './Checkbox';
-export { Form } from './Form';
-export { Input } from './Input';
-export { Modal } from './Modal';
-export { Select } from './Select';
-export { Table } from './Table';
+import { Page } from '@playwright/test';
+import { Modal } from '../core/Modal';
+
+/**
+ * Confirm Dialog component for Ant Design Modal.confirm dialogs.
+ * These are the "OK" / "Cancel" confirmation dialogs used throughout Superset.
+ */
+export class ConfirmDialog extends Modal {
+  constructor(page: Page) {
+    // Modal.confirm uses the same [role="dialog"] selector
+    super(page);
+  }
+
+  /**
+   * Clicks the OK button to confirm
+   */
+  async clickOk(): Promise<void> {
+    await this.clickFooterButton('OK');
+  }
+
+  /**
+   * Clicks the Cancel button to dismiss
+   */
+  async clickCancel(): Promise<void> {
+    await this.clickFooterButton('Cancel');
+  }
+}
diff --git a/superset-frontend/playwright/components/modals/EditDatasetModal.ts 
b/superset-frontend/playwright/components/modals/EditDatasetModal.ts
new file mode 100644
index 0000000000..a667378912
--- /dev/null
+++ b/superset-frontend/playwright/components/modals/EditDatasetModal.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Page } from '@playwright/test';
+import { Modal, Tabs, Textarea } from '../core';
+
+/**
+ * Edit Dataset Modal component (DatasourceModal).
+ * Used for editing dataset properties like description, metrics, columns, etc.
+ */
+export class EditDatasetModal extends Modal {
+  private readonly tabs: Tabs;
+
+  constructor(page: Page) {
+    super(page, 'Edit Dataset');
+    this.tabs = new Tabs(page);
+  }
+
+  /**
+   * Click the Save button to save changes
+   */
+  async clickSave(): Promise<void> {
+    await this.clickFooterButton('Save');
+  }
+
+  /**
+   * Click the Cancel button to discard changes
+   */
+  async clickCancel(): Promise<void> {
+    await this.clickFooterButton('Cancel');
+  }
+
+  /**
+   * Get the description textarea component
+   */
+  getDescriptionTextarea(): Textarea {
+    return Textarea.fromName(this.page, 'description');
+  }
+
+  /**
+   * Fill in the description field
+   * @param description - The description text to enter
+   */
+  async fillDescription(description: string): Promise<void> {
+    await this.getDescriptionTextarea().fill(description);
+  }
+
+  /**
+   * Navigate to a specific tab in the modal
+   * @param tabName - The name of the tab (e.g., 'Source', 'Metrics', 
'Columns')
+   */
+  async clickTab(tabName: string): Promise<void> {
+    await this.tabs.clickTab(tabName);
+  }
+}
diff --git a/superset-frontend/playwright/pages/DatasetListPage.ts 
b/superset-frontend/playwright/pages/DatasetListPage.ts
index 89662021be..86a2802461 100644
--- a/superset-frontend/playwright/pages/DatasetListPage.ts
+++ b/superset-frontend/playwright/pages/DatasetListPage.ts
@@ -31,6 +31,7 @@ export class DatasetListPage {
   private static readonly SELECTORS = {
     DATASET_LINK: '[data-test="internal-link"]',
     DELETE_ACTION: '.action-button svg[data-icon="delete"]',
+    EDIT_ACTION: '.action-button svg[data-icon="edit"]',
     EXPORT_ACTION: '.action-button svg[data-icon="upload"]',
     DUPLICATE_ACTION: '.action-button svg[data-icon="copy"]',
     BULK_SELECT_CONTROLS: '[data-test="bulk-select-controls"]',
@@ -93,6 +94,17 @@ export class DatasetListPage {
     );
   }
 
+  /**
+   * Clicks the edit action button for a dataset
+   * @param datasetName - The name of the dataset to edit
+   */
+  async clickEditAction(datasetName: string): Promise<void> {
+    await this.table.clickRowAction(
+      datasetName,
+      DatasetListPage.SELECTORS.EDIT_ACTION,
+    );
+  }
+
   /**
    * Clicks the export action button for a dataset
    * @param datasetName - The name of the dataset to export
diff --git 
a/superset-frontend/playwright/tests/experimental/dataset/dataset-list.spec.ts 
b/superset-frontend/playwright/tests/experimental/dataset/dataset-list.spec.ts
index d9dd1d23bb..5129403ad7 100644
--- 
a/superset-frontend/playwright/tests/experimental/dataset/dataset-list.spec.ts
+++ 
b/superset-frontend/playwright/tests/experimental/dataset/dataset-list.spec.ts
@@ -21,8 +21,10 @@ import { test, expect } from '@playwright/test';
 import { DatasetListPage } from '../../../pages/DatasetListPage';
 import { ExplorePage } from '../../../pages/ExplorePage';
 import { CreateDatasetPage } from '../../../pages/CreateDatasetPage';
+import { ConfirmDialog } from '../../../components/modals/ConfirmDialog';
 import { DeleteConfirmationModal } from 
'../../../components/modals/DeleteConfirmationModal';
 import { DuplicateDatasetModal } from 
'../../../components/modals/DuplicateDatasetModal';
+import { EditDatasetModal } from '../../../components/modals/EditDatasetModal';
 import { Toast } from '../../../components/core/Toast';
 import {
   apiDeleteDataset,
@@ -358,3 +360,54 @@ test('should navigate the create dataset wizard', async ({ 
page }) => {
     createDatasetPage.getCreateAndExploreButton().element,
   ).toBeVisible();
 });
+
+test('should edit a dataset description', async ({ page }) => {
+  const editModal = new EditDatasetModal(page);
+  const confirmDialog = new ConfirmDialog(page);
+  const toast = new Toast(page);
+
+  // Use existing example dataset
+  const datasetName = TEST_DATASETS.EXAMPLE_DATASET;
+
+  // Verify dataset is visible in list
+  await expect(datasetListPage.getDatasetRow(datasetName)).toBeVisible();
+
+  // Click edit action button
+  await datasetListPage.clickEditAction(datasetName);
+
+  // Edit modal should appear
+  await editModal.waitForVisible();
+
+  // Generate unique description to verify edit works
+  const newDescription = `Test description updated at ${Date.now()}`;
+
+  // Fill in new description
+  await editModal.fillDescription(newDescription);
+
+  // Click Save button
+  await editModal.clickSave();
+
+  // Confirm the save in the confirmation dialog
+  await confirmDialog.waitForVisible();
+  await confirmDialog.clickOk();
+
+  // Modal should close
+  await editModal.waitForHidden();
+
+  // Verify success toast appears
+  const successToast = toast.getSuccess();
+  await expect(successToast).toBeVisible();
+
+  // Verify the description was saved by re-opening the edit modal
+  await datasetListPage.clickEditAction(datasetName);
+  await editModal.waitForVisible();
+
+  // Verify description textarea contains our new description
+  await expect(editModal.getDescriptionTextarea().element).toHaveValue(
+    newDescription,
+  );
+
+  // Close modal without saving
+  await editModal.clickCancel();
+  await editModal.waitForHidden();
+});

Reply via email to