Skip to content

Commit 39e8998

Browse files
sadpandajoeclaude
andcommitted
fix(sqllab): fix Playwright tests failing in CI empty-state SQL Lab
In CI, the admin user has no saved SQL Lab tabs, so TabbedSqlEditors renders type="card" (empty state) instead of type="editable-card". The antd add-tab button (.ant-tabs-nav-add) only exists in editable-card mode, causing all 10 tests to timeout at 30s. - Add EditableTabs Playwright component for antd editable-card tabs - Refactor SqlLabPage to use EditableTabs for tab management - ensureEditorReady() clicks [data-test="add-tab-icon"] which exists in both card modes, replacing the missing getByRole button - Replace fixed 1000ms wait with proper waitFor({ state: 'visible' }) - Add test.setTimeout(60_000) to all SQL Lab specs for CI headroom Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 427c09c commit 39e8998

7 files changed

Lines changed: 103 additions & 37 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { Tabs } from './Tabs';
21+
22+
/**
23+
* EditableTabs component for Ant Design editable-card tabs.
24+
*
25+
* Mirrors the Superset EditableTabs component (type="editable-card")
26+
* which adds add/remove tab functionality to the base Tabs component.
27+
*
28+
* The add button (.ant-tabs-nav-add) is only rendered when
29+
* type="editable-card". If the host component switches to type="card"
30+
* (e.g., SQL Lab empty state), use the host page object for that case.
31+
*/
32+
export class EditableTabs extends Tabs {
33+
/**
34+
* Clicks the add-tab button rendered by antd in editable-card mode.
35+
*/
36+
async addTab(): Promise<void> {
37+
await this.element
38+
.getByRole('button', { name: 'Add tab' })
39+
.click();
40+
}
41+
42+
/**
43+
* Returns the number of tabs (excludes the add button).
44+
*/
45+
async getTabCount(): Promise<number> {
46+
return this.element.getByRole('tab').count();
47+
}
48+
49+
/**
50+
* Returns the text content of all tabs.
51+
*/
52+
async getTabNames(): Promise<string[]> {
53+
return this.element.getByRole('tab').allTextContents();
54+
}
55+
56+
/**
57+
* Clicks the remove button on the last tab.
58+
*/
59+
async removeLastTab(): Promise<void> {
60+
await this.element
61+
.locator('.ant-tabs-tab-remove')
62+
.last()
63+
.click();
64+
}
65+
}

superset-frontend/playwright/components/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export { AceEditor } from './AceEditor';
2222
export { AgGrid } from './AgGrid';
2323
export { Button } from './Button';
2424
export { Checkbox } from './Checkbox';
25+
export { EditableTabs } from './EditableTabs';
2526
export { Form } from './Form';
2627
export { Input } from './Input';
2728
export { Menu } from './Menu';

superset-frontend/playwright/pages/SqlLabPage.ts

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import { Page, Locator } from '@playwright/test';
2121
import { AceEditor } from '../components/core/AceEditor';
2222
import { AgGrid } from '../components/core/AgGrid';
23+
import { EditableTabs } from '../components/core/EditableTabs';
2324
import { Select } from '../components/core/Select';
2425
import { Modal } from '../components/core/Modal';
2526
import { URL } from '../utils/urls';
@@ -32,14 +33,11 @@ import { TIMEOUT } from '../utils/constants';
3233
*/
3334
export class SqlLabPage {
3435
private readonly page: Page;
36+
private readonly editorTabs: EditableTabs;
3537

3638
private static readonly SELECTORS = {
3739
SQL_EDITOR_TABS: '[data-test="sql-editor-tabs"]',
38-
TABLIST: '[data-test="sql-editor-tabs"] > [role="tablist"]',
39-
TAB: '[data-test="sql-editor-tabs"] > [role="tablist"] [role="tab"]:not([type="button"])',
4040
ADD_TAB_ICON: '[data-test="add-tab-icon"]',
41-
DROPDOWN_TRIGGER: '[data-test="dropdown-trigger"]',
42-
CLOSE_TAB_MENU_OPTION: '[data-test="close-tab-menu-option"]',
4341
RUN_QUERY_BUTTON: '[data-test="run-query-action"]',
4442
SOUTH_PANE: '[data-test="south-pane"]',
4543
EXPLORE_RESULTS_BUTTON: '[data-test="explore-results-button"]',
@@ -49,12 +47,15 @@ export class SqlLabPage {
4947
LEFT_BAR: '[data-test="sql-editor-left-bar"]',
5048
DATABASE_SELECTOR: '[data-test="DatabaseSelector"]',
5149
LIMIT_DROPDOWN: '.limitDropdown',
52-
TAB_REMOVE: '[aria-label="remove"]',
5350
SAVE_DATASET_BUTTON: 'button[aria-label="Save dataset"]',
5451
} as const;
5552

5653
constructor(page: Page) {
5754
this.page = page;
55+
this.editorTabs = new EditableTabs(
56+
page,
57+
page.locator(SqlLabPage.SELECTORS.SQL_EDITOR_TABS),
58+
);
5859
}
5960

6061
// ── Navigation ──
@@ -66,23 +67,29 @@ export class SqlLabPage {
6667
async waitForPageLoad(options?: { timeout?: number }): Promise<void> {
6768
// SQL Lab with dev server can be slow on first load (webpack HMR + React hydration)
6869
const timeout = options?.timeout ?? TIMEOUT.QUERY_EXECUTION;
69-
await this.page
70-
.locator(SqlLabPage.SELECTORS.SQL_EDITOR_TABS)
71-
.waitFor({ state: 'visible', timeout });
70+
await this.editorTabs.element.waitFor({ state: 'visible', timeout });
7271
}
7372

7473
/**
7574
* Ensures at least one query editor tab exists. Creates one if SQL Lab
7675
* is in the empty state ("Add a new tab to create SQL Query").
7776
* Waits for the ace editor to be ready before returning.
77+
*
78+
* SQL Lab renders type="card" in empty state (no .ant-tabs-nav-add button),
79+
* so we click the SQL Lab-specific [data-test="add-tab-icon"] instead of
80+
* using EditableTabs.addTab() which requires type="editable-card".
7881
*/
7982
async ensureEditorReady(): Promise<void> {
8083
const editorLocator = this.page.locator(SqlLabPage.SELECTORS.ACE_EDITOR);
8184
const editorCount = await editorLocator.count();
8285
if (editorCount === 0) {
83-
await this.addTab();
84-
// Wait for new tab panel to render
85-
await this.page.waitForTimeout(1000);
86+
// Empty state: click the add-tab icon directly (works in both card modes)
87+
await this.editorTabs.element
88+
.locator(SqlLabPage.SELECTORS.ADD_TAB_ICON)
89+
.first()
90+
.click();
91+
// Wait for ace editor to render after tab creation
92+
await editorLocator.first().waitFor({ state: 'visible' });
8693
}
8794
await this.getEditor().waitForReady();
8895
}
@@ -120,25 +127,16 @@ export class SqlLabPage {
120127

121128
// ── Tab Management ──
122129

123-
private get tabs(): Locator {
124-
return this.page.locator(SqlLabPage.SELECTORS.TAB);
125-
}
126-
127130
async getTabCount(): Promise<number> {
128-
return this.tabs.count();
131+
return this.editorTabs.getTabCount();
129132
}
130133

131134
async getTabNames(): Promise<string[]> {
132-
return this.tabs.allTextContents();
135+
return this.editorTabs.getTabNames();
133136
}
134137

135138
async addTab(): Promise<void> {
136-
// Target the visible "Add tab" button — there can be duplicates in the DOM
137-
await this.page
138-
.locator(SqlLabPage.SELECTORS.SQL_EDITOR_TABS)
139-
.getByRole('button', { name: 'Add tab' })
140-
.first()
141-
.click();
139+
await this.editorTabs.addTab();
142140
}
143141

144142
async addTabByShortcut(): Promise<void> {
@@ -148,24 +146,24 @@ export class SqlLabPage {
148146

149147
async closeLastTab(): Promise<void> {
150148
const countBefore = await this.getTabCount();
151-
// Click the × (close) button on the last tab.
152-
await this.page
153-
.locator(
154-
`${SqlLabPage.SELECTORS.TABLIST} ${SqlLabPage.SELECTORS.TAB_REMOVE}`,
155-
)
156-
.last()
157-
.click();
149+
await this.editorTabs.removeLastTab();
158150
// Wait for tab count to decrease
159-
const tabSelector = SqlLabPage.SELECTORS.TAB;
160151
await this.page.waitForFunction(
161-
([sel, expected]) => document.querySelectorAll(sel).length === expected,
162-
[tabSelector, countBefore - 1] as const,
152+
([count, expected]) => {
153+
const tabs = document.querySelectorAll(
154+
'[data-test="sql-editor-tabs"] [role="tab"]',
155+
);
156+
return tabs.length === expected;
157+
},
158+
[countBefore, countBefore - 1] as const,
163159
{ timeout: 5000 },
164160
);
165161
}
166162

167163
getTab(name: string): Locator {
168-
return this.page.locator('[role="tab"]', { hasText: name });
164+
// Use hasText (substring) rather than getByRole name (accessible name)
165+
// because getTabNames() returns text content which may include close-icon text.
166+
return this.editorTabs.element.locator('[role="tab"]', { hasText: name });
169167
}
170168

171169
// ── Database Selection (Left Sidebar) ──

superset-frontend/playwright/tests/sqllab/create-chart-from-query.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { TIMEOUT } from '../../utils/constants';
2626
test('should navigate to Explore from SQL Lab query results', async ({
2727
page,
2828
}) => {
29+
test.setTimeout(60_000);
2930
const sqlLabPage = new SqlLabPage(page);
3031
await sqlLabPage.goto();
3132
await sqlLabPage.waitForPageLoad();

superset-frontend/playwright/tests/sqllab/query-execution.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { TIMEOUT } from '../../utils/constants';
2525
let sqlLabPage: SqlLabPage;
2626

2727
test.beforeEach(async ({ page }) => {
28+
test.setTimeout(60_000);
2829
sqlLabPage = new SqlLabPage(page);
2930
await sqlLabPage.goto();
3031
await sqlLabPage.waitForPageLoad();
@@ -56,9 +57,7 @@ test('executes a simple SELECT query and displays results', async ({
5657
});
5758

5859
test('shows error message for invalid SQL', async ({ page }) => {
59-
await sqlLabPage.setQuery(
60-
'SELECT * FROM a_table_that_does_not_exist_xyz_pw',
61-
);
60+
await sqlLabPage.setQuery('SELECT * FROM a_table_that_does_not_exist_xyz_pw');
6261

6362
const executePromise = waitForPost(page, 'api/v1/sqllab/execute/', {
6463
timeout: TIMEOUT.QUERY_EXECUTION,

superset-frontend/playwright/tests/sqllab/save-and-share.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { TIMEOUT } from '../../utils/constants';
2828
let sqlLabPage: SqlLabPage;
2929

3030
test.beforeEach(async ({ page }) => {
31+
test.setTimeout(60_000);
3132
sqlLabPage = new SqlLabPage(page);
3233
await sqlLabPage.goto();
3334
await sqlLabPage.waitForPageLoad();

superset-frontend/playwright/tests/sqllab/tabs.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { waitForPost } from '../../helpers/api/intercepts';
2424
let sqlLabPage: SqlLabPage;
2525

2626
test.beforeEach(async ({ page }) => {
27+
test.setTimeout(60_000);
2728
sqlLabPage = new SqlLabPage(page);
2829
await sqlLabPage.goto();
2930
await sqlLabPage.waitForPageLoad();

0 commit comments

Comments
 (0)