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(); +});
