You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2026/04/02 18:04:40 UTC
(superset) 02/02: test(sqllab): align Playwright tests with sc-102088 story requirements
This is an automated email from the ASF dual-hosted git repository.
jli pushed a commit to branch feat-sql-lab-to-playwright
in repository https://gitbox.apache.org/repos/asf/superset.git
commit b0a82b948810002c071c6db015a14ed593cba703
Author: Joe Li <jo...@preset.io>
AuthorDate: Thu Apr 2 09:40:41 2026 -0700
test(sqllab): align Playwright tests with sc-102088 story requirements
Add 5 new tests and reorganize files to match the story spec:
- query-execution.spec.ts: error handling + re-run query tests
- tabs.spec.ts: create/close/preserve-state tests
- save-and-share.spec.ts: moved save query + new dataset creation test
- SqlLabPage POM: error alert, save dataset button/modal methods
Co-Authored-By: Claude Opus 4.6 (1M context) <no...@anthropic.com>
---
superset-frontend/playwright/pages/SqlLabPage.ts | 20 ++++
.../tests/sqllab/query-execution.spec.ts | 81 ++++++++---------
...ry-execution.spec.ts => save-and-share.spec.ts} | 101 ++++++++++++++++-----
.../playwright/tests/sqllab/tabs.spec.ts | 72 ++++++++++-----
4 files changed, 184 insertions(+), 90 deletions(-)
diff --git a/superset-frontend/playwright/pages/SqlLabPage.ts b/superset-frontend/playwright/pages/SqlLabPage.ts
index bfe60f9e1ba..1b6fea415f8 100644
--- a/superset-frontend/playwright/pages/SqlLabPage.ts
+++ b/superset-frontend/playwright/pages/SqlLabPage.ts
@@ -50,6 +50,7 @@ export class SqlLabPage {
DATABASE_SELECTOR: '[data-test="DatabaseSelector"]',
LIMIT_DROPDOWN: '.limitDropdown',
TAB_REMOVE: '[aria-label="remove"]',
+ SAVE_DATASET_BUTTON: 'button[aria-label="Save dataset"]',
} as const;
constructor(page: Page) {
@@ -237,6 +238,10 @@ export class SqlLabPage {
return this.page.locator(SqlLabPage.SELECTORS.SOUTH_PANE);
}
+ getErrorAlert(): Locator {
+ return this.getResultsPane().locator('.ant-alert-error');
+ }
+
// ── Row Limit ──
async getRowLimit(): Promise<string> {
@@ -261,6 +266,21 @@ export class SqlLabPage {
return new Modal(this.page, SqlLabPage.SELECTORS.SAVE_QUERY_MODAL);
}
+ // ── Save Dataset ──
+
+ async clickSaveDatasetButton(): Promise<void> {
+ await this.activePanel
+ .locator(SqlLabPage.SELECTORS.SAVE_DATASET_BUTTON)
+ .click();
+ }
+
+ getSaveDatasetModal(): Modal {
+ return new Modal(
+ this.page,
+ '[data-test="Save or Overwrite Dataset-modal"] .ant-modal',
+ );
+ }
+
// ── Create Chart ──
getCreateChartButton(): Locator {
diff --git a/superset-frontend/playwright/tests/sqllab/query-execution.spec.ts b/superset-frontend/playwright/tests/sqllab/query-execution.spec.ts
index 2ccfa0ed392..0cfdef1254a 100644
--- a/superset-frontend/playwright/tests/sqllab/query-execution.spec.ts
+++ b/superset-frontend/playwright/tests/sqllab/query-execution.spec.ts
@@ -17,10 +17,9 @@
* under the License.
*/
-import { test, expect } from '../../helpers/fixtures/testAssets';
+import { test, expect } from '@playwright/test';
import { SqlLabPage } from '../../pages/SqlLabPage';
import { waitForPost } from '../../helpers/api/intercepts';
-import { apiGetSavedQuery } from '../../helpers/api/savedQuery';
import { TIMEOUT } from '../../utils/constants';
let sqlLabPage: SqlLabPage;
@@ -32,7 +31,9 @@ test.beforeEach(async ({ page }) => {
await sqlLabPage.ensureEditorReady();
});
-test('should execute a query and display results', async ({ page }) => {
+test('executes a simple SELECT query and displays results', async ({
+ page,
+}) => {
// Verify the left sidebar database selector is visible and interactive (#38833)
await expect(sqlLabPage.getDatabaseSelectorText()).toBeVisible();
@@ -54,57 +55,51 @@ test('should execute a query and display results', async ({ page }) => {
expect(headers.some(h => h.includes('test_col'))).toBe(true);
});
-test('should save and reload a query', async ({ page, testAssets }) => {
- const queryText = 'SELECT 1 AS saved_test_col';
- const savedQueryTitle = `pw_test_saved_query_${Date.now()}`;
-
- // Verify left sidebar is interactive
- await expect(sqlLabPage.getDatabaseSelectorText()).toBeVisible();
-
- // Set and run query
- await sqlLabPage.setQuery(queryText);
+test('shows error message for invalid SQL', async ({ page }) => {
+ await sqlLabPage.setQuery(
+ 'SELECT * FROM a_table_that_does_not_exist_xyz_pw',
+ );
const executePromise = waitForPost(page, 'api/v1/sqllab/execute/', {
timeout: TIMEOUT.QUERY_EXECUTION,
});
await sqlLabPage.runQuery();
await executePromise;
- await sqlLabPage.waitForQueryResults();
- // Open the save query modal
- await sqlLabPage.clickSaveButton();
- const saveModal = sqlLabPage.getSaveQueryModal();
- await saveModal.waitForReady();
+ // Wait for error alert to render in south pane
+ const errorAlert = sqlLabPage.getErrorAlert();
+ await expect(errorAlert).toBeVisible({ timeout: TIMEOUT.QUERY_EXECUTION });
- // Fill in the query name
- await saveModal.body.locator('input[type="text"]').first().clear();
- await saveModal.body
- .locator('input[type="text"]')
- .first()
- .fill(savedQueryTitle);
+ // Verify the south pane contains an error indicator (engine-agnostic)
+ const southPane = sqlLabPage.getResultsPane();
+ await expect(southPane).toContainText(/error/i);
+});
- // Save and intercept the API response
- const savePromise = waitForPost(page, 'api/v1/saved_query/', {
- timeout: TIMEOUT.API_RESPONSE,
+test('re-runs a query and refreshes results', async ({ page }) => {
+ // First query
+ await sqlLabPage.setQuery('SELECT 1 AS first_col');
+ const firstExecute = waitForPost(page, 'api/v1/sqllab/execute/', {
+ timeout: TIMEOUT.QUERY_EXECUTION,
});
- await saveModal.footer
- .getByRole('button', { name: 'Save', exact: true })
- .click();
- const saveResponse = await savePromise;
- expect(saveResponse.status()).toBe(201);
+ await sqlLabPage.runQuery();
+ const firstResponse = await firstExecute;
+ expect(firstResponse.status()).toBe(200);
+ await sqlLabPage.waitForQueryResults();
- // Extract saved query ID for cleanup
- const saveBody = await saveResponse.json();
- const savedQueryId: number = saveBody.id ?? saveBody.result?.id;
- expect(savedQueryId).toBeTruthy();
- testAssets.trackSavedQuery(savedQueryId);
+ const firstHeaders = await sqlLabPage.getResultsGrid().getHeaderTexts();
+ expect(firstHeaders.some(h => h.includes('first_col'))).toBe(true);
- // Verify the modal closed
- await saveModal.waitForHidden();
+ // Second query (re-run with different SQL)
+ await sqlLabPage.setQuery('SELECT 2 AS second_col');
+ const secondExecute = waitForPost(page, 'api/v1/sqllab/execute/', {
+ timeout: TIMEOUT.QUERY_EXECUTION,
+ });
+ await sqlLabPage.runQuery();
+ const secondResponse = await secondExecute;
+ expect(secondResponse.status()).toBe(200);
+ await sqlLabPage.waitForQueryResults();
- // Verify the saved query via API (round-trip persistence check)
- const getResponse = await apiGetSavedQuery(page, savedQueryId);
- const savedQuery = (await getResponse.json()).result;
- expect(savedQuery.sql).toContain('saved_test_col');
- expect(savedQuery.label).toBe(savedQueryTitle);
+ const secondHeaders = await sqlLabPage.getResultsGrid().getHeaderTexts();
+ expect(secondHeaders.some(h => h.includes('second_col'))).toBe(true);
+ expect(secondHeaders.some(h => h.includes('first_col'))).toBe(false);
});
diff --git a/superset-frontend/playwright/tests/sqllab/query-execution.spec.ts b/superset-frontend/playwright/tests/sqllab/save-and-share.spec.ts
similarity index 57%
copy from superset-frontend/playwright/tests/sqllab/query-execution.spec.ts
copy to superset-frontend/playwright/tests/sqllab/save-and-share.spec.ts
index 2ccfa0ed392..2f5c2b3bc37 100644
--- a/superset-frontend/playwright/tests/sqllab/query-execution.spec.ts
+++ b/superset-frontend/playwright/tests/sqllab/save-and-share.spec.ts
@@ -17,8 +17,10 @@
* under the License.
*/
-import { test, expect } from '../../helpers/fixtures/testAssets';
+import { expect } from '@playwright/test';
+import { test } from '../../helpers/fixtures/testAssets';
import { SqlLabPage } from '../../pages/SqlLabPage';
+import { ExplorePage } from '../../pages/ExplorePage';
import { waitForPost } from '../../helpers/api/intercepts';
import { apiGetSavedQuery } from '../../helpers/api/savedQuery';
import { TIMEOUT } from '../../utils/constants';
@@ -32,29 +34,10 @@ test.beforeEach(async ({ page }) => {
await sqlLabPage.ensureEditorReady();
});
-test('should execute a query and display results', async ({ page }) => {
- // Verify the left sidebar database selector is visible and interactive (#38833)
- await expect(sqlLabPage.getDatabaseSelectorText()).toBeVisible();
-
- // Set the query
- await sqlLabPage.setQuery('SELECT 1 AS test_col');
-
- // Run query and wait for API response
- const executePromise = waitForPost(page, 'api/v1/sqllab/execute/', {
- timeout: TIMEOUT.QUERY_EXECUTION,
- });
- await sqlLabPage.runQuery();
- const response = await executePromise;
- expect(response.status()).toBe(200);
-
- // Verify results appear in the AG Grid
- await sqlLabPage.waitForQueryResults();
- const grid = sqlLabPage.getResultsGrid();
- const headers = await grid.getHeaderTexts();
- expect(headers.some(h => h.includes('test_col'))).toBe(true);
-});
-
-test('should save and reload a query', async ({ page, testAssets }) => {
+test('saves a query and loads it from saved queries', async ({
+ page,
+ testAssets,
+}) => {
const queryText = 'SELECT 1 AS saved_test_col';
const savedQueryTitle = `pw_test_saved_query_${Date.now()}`;
@@ -108,3 +91,73 @@ test('should save and reload a query', async ({ page, testAssets }) => {
expect(savedQuery.sql).toContain('saved_test_col');
expect(savedQuery.label).toBe(savedQueryTitle);
});
+
+test('creates a dataset from query results', async ({ page, testAssets }) => {
+ // Select database to enable the "Save dataset" toolbar button
+ await sqlLabPage.selectDatabase('examples');
+
+ const queryText = 'SELECT 1 AS ds_test_col';
+ await sqlLabPage.setQuery(queryText);
+
+ const executePromise = waitForPost(page, 'api/v1/sqllab/execute/', {
+ timeout: TIMEOUT.QUERY_EXECUTION,
+ });
+ await sqlLabPage.runQuery();
+ const executeResponse = await executePromise;
+
+ if (executeResponse.status() !== 200) {
+ test.skip(true, 'Query execution failed — database may not be configured');
+ }
+
+ await sqlLabPage.waitForQueryResults();
+
+ // Click "Save dataset" button in the toolbar
+ await sqlLabPage.clickSaveDatasetButton();
+
+ // Wait for the Save Dataset modal
+ const saveDatasetModal = sqlLabPage.getSaveDatasetModal();
+ await saveDatasetModal.waitForReady();
+
+ // Fill in a unique dataset name
+ const datasetName = `pw_test_dataset_${Date.now()}`;
+ const nameInput = saveDatasetModal.body.locator(
+ 'input[placeholder="Dataset name"]',
+ );
+ await nameInput.clear();
+ await nameInput.fill(datasetName);
+
+ // Set up intercepts before clicking save:
+ // 1. Dataset creation API
+ // 2. New browser tab (SaveDatasetModal opens Explore via window.open)
+ const datasetCreatePromise = waitForPost(page, 'api/v1/dataset/', {
+ timeout: TIMEOUT.API_RESPONSE,
+ });
+ const newPagePromise = page.context().waitForEvent('page', {
+ timeout: TIMEOUT.API_RESPONSE,
+ });
+
+ // Click "Save & Explore"
+ await saveDatasetModal.footer
+ .getByRole('button', { name: /Save & Explore/i })
+ .click();
+
+ // Capture dataset ID for cleanup
+ const createResponse = await datasetCreatePromise;
+ const createBody = await createResponse.json();
+ const datasetId: number = createBody.result?.id ?? createBody.id;
+ expect(datasetId).toBeTruthy();
+ testAssets.trackDataset(datasetId);
+
+ // Wait for the new tab with Explore page
+ const newPage = await newPagePromise;
+ await newPage.waitForLoadState();
+
+ const explorePage = new ExplorePage(newPage);
+ await explorePage.waitForPageLoad({ timeout: TIMEOUT.PAGE_LOAD });
+
+ // Verify the dataset name appears in the Explore datasource control
+ const loadedDatasetName = await explorePage.getDatasetName();
+ expect(loadedDatasetName).toContain(datasetName);
+
+ await newPage.close();
+});
diff --git a/superset-frontend/playwright/tests/sqllab/tabs.spec.ts b/superset-frontend/playwright/tests/sqllab/tabs.spec.ts
index a687815ebb1..1efeb941274 100644
--- a/superset-frontend/playwright/tests/sqllab/tabs.spec.ts
+++ b/superset-frontend/playwright/tests/sqllab/tabs.spec.ts
@@ -30,44 +30,70 @@ test.beforeEach(async ({ page }) => {
await sqlLabPage.ensureEditorReady();
});
-test('should create tabs that persist across page reload', async ({
- page,
-}) => {
+test('creates a new tab via button', async () => {
const initialTabCount = await sqlLabPage.getTabCount();
- // Create a new tab and verify count incremented
await sqlLabPage.addTab();
await sqlLabPage.getEditor().waitForReady();
- expect(await sqlLabPage.getTabCount()).toBe(initialTabCount + 1);
- // Set up tab state intercept before making changes (avoid race with debounced save)
- const tabStatePromise = waitForPost(page, 'tabstateview');
+ expect(await sqlLabPage.getTabCount()).toBe(initialTabCount + 1);
- // Type a distinctive query so we can verify persistence
- const testQuery = `SELECT 'persistence_test_${Date.now()}'`;
- await sqlLabPage.setQuery(testQuery);
+ // Verify new tab has default SQL content
+ const defaultContent = await sqlLabPage.getQuery();
+ expect(defaultContent).toContain('SELECT');
- // Trigger a save by blurring the editor (tab state saves on editor changes)
- await page.locator('body').click();
- await tabStatePromise;
+ // Clean up
+ await sqlLabPage.closeLastTab();
+ expect(await sqlLabPage.getTabCount()).toBe(initialTabCount);
+});
- // Reload the page
- await page.reload();
- await sqlLabPage.waitForPageLoad();
- await sqlLabPage.ensureEditorReady();
+test('closes a tab via close button', async () => {
+ const initialTabCount = await sqlLabPage.getTabCount();
- // Verify the tab survived the reload
+ // Create a tab so we have something to close
+ await sqlLabPage.addTab();
+ await sqlLabPage.getEditor().waitForReady();
expect(await sqlLabPage.getTabCount()).toBe(initialTabCount + 1);
- // Verify the editor content survived the reload
- const restoredQuery = await sqlLabPage.getQuery();
- expect(restoredQuery).toContain('persistence_test_');
-
- // Clean up: close the tab we created
+ // Close the tab via the × button
await sqlLabPage.closeLastTab();
expect(await sqlLabPage.getTabCount()).toBe(initialTabCount);
});
+test('preserves query state when switching tabs', async () => {
+ const tabOneSql = `SELECT 'tab_one_${Date.now()}'`;
+ const tabTwoSql = `SELECT 'tab_two_${Date.now()}'`;
+
+ // Get the first tab's name for switching back later
+ const tabNames = await sqlLabPage.getTabNames();
+ const firstTabName = tabNames[tabNames.length - 1];
+
+ // Set query in the first (current) tab
+ await sqlLabPage.setQuery(tabOneSql);
+
+ // Create second tab and set a different query
+ await sqlLabPage.addTab();
+ await sqlLabPage.getEditor().waitForReady();
+ await sqlLabPage.setQuery(tabTwoSql);
+
+ // Switch back to first tab and verify its content is preserved
+ await sqlLabPage.getTab(firstTabName).click();
+ await sqlLabPage.getEditor().waitForReady();
+ const firstContent = await sqlLabPage.getQuery();
+ expect(firstContent).toContain('tab_one_');
+
+ // Switch to second tab and verify its content is preserved
+ const updatedNames = await sqlLabPage.getTabNames();
+ const secondTabName = updatedNames[updatedNames.length - 1];
+ await sqlLabPage.getTab(secondTabName).click();
+ await sqlLabPage.getEditor().waitForReady();
+ const secondContent = await sqlLabPage.getQuery();
+ expect(secondContent).toContain('tab_two_');
+
+ // Clean up
+ await sqlLabPage.closeLastTab();
+});
+
test('should open new tab by keyboard shortcut with correct defaults', async ({
page,
}) => {