Skip to content

Commit 31754b3

Browse files
committed
test(sqllab): add Playwright POM and helpers for SQL Lab E2E tests
SqlLabPage page object with AceEditor, AgGrid, DatabaseSelector, and EditableTabs component wrappers. Shared test helpers for API assertions (expectStatus, extractIdFromResponse), saved-query API, and request intercepts (waitForPost/waitForGet). SaveQueryModal and SaveDatasetModal page objects. Timeout constants and URL registry.
1 parent 680cef0 commit 31754b3

18 files changed

Lines changed: 812 additions & 12 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 { Locator, Page } from '@playwright/test';
21+
22+
const AG_GRID_SELECTORS = {
23+
ROOT: '[role="grid"]',
24+
HEADER_ROW: '.ag-header-row',
25+
HEADER_CELL: '.ag-header-cell',
26+
BODY_ROW: '.ag-row',
27+
CELL: '.ag-cell',
28+
} as const;
29+
30+
/**
31+
* AG Grid component wrapper for Playwright.
32+
* Used by FilterableTable/GridTable in SQL Lab results and elsewhere.
33+
*/
34+
export class AgGrid {
35+
readonly page: Page;
36+
private readonly locator: Locator;
37+
38+
constructor(page: Page, locator: Locator) {
39+
this.page = page;
40+
this.locator = locator;
41+
}
42+
43+
get element(): Locator {
44+
return this.locator;
45+
}
46+
47+
/**
48+
* Wait for the grid to render with data rows
49+
*/
50+
async waitForRows(options?: { timeout?: number }): Promise<void> {
51+
await this.locator
52+
.locator(AG_GRID_SELECTORS.BODY_ROW)
53+
.first()
54+
.waitFor({ state: 'visible', ...options });
55+
}
56+
57+
/**
58+
* Get header cell texts
59+
*/
60+
async getHeaderTexts(): Promise<string[]> {
61+
return this.locator
62+
.locator(AG_GRID_SELECTORS.HEADER_CELL)
63+
.allTextContents();
64+
}
65+
66+
/**
67+
* Get the number of visible data rows
68+
*/
69+
async getRowCount(): Promise<number> {
70+
return this.locator.locator(AG_GRID_SELECTORS.BODY_ROW).count();
71+
}
72+
73+
/**
74+
* Get cell text at a specific row and column index (0-based)
75+
*/
76+
async getCellText(row: number, col: number): Promise<string> {
77+
const text = await this.locator
78+
.locator(AG_GRID_SELECTORS.BODY_ROW)
79+
.nth(row)
80+
.locator(AG_GRID_SELECTORS.CELL)
81+
.nth(col)
82+
.textContent();
83+
return text?.trim() ?? '';
84+
}
85+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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.getByRole('button', { name: 'Add tab' }).click();
38+
}
39+
40+
/**
41+
* Clicks the remove button on the last tab.
42+
*/
43+
async removeLastTab(): Promise<void> {
44+
await this.nav.locator('.ant-tabs-tab-remove').last().click();
45+
}
46+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class Modal {
6868
}
6969

7070
/**
71-
* Gets a footer button by text content (private helper)
71+
* Gets a footer button by text content
7272
* @param buttonText - The text content of the button
7373
*/
7474
private getFooterButton(buttonText: string): Locator {
@@ -80,7 +80,7 @@ export class Modal {
8080
* @param buttonText - The text content of the button to click
8181
* @param options - Optional click options
8282
*/
83-
protected async clickFooterButton(
83+
async clickFooterButton(
8484
buttonText: string,
8585
options?: { timeout?: number; force?: boolean; delay?: number },
8686
): Promise<void> {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 { Locator, Page } from '@playwright/test';
21+
import { Button } from './Button';
22+
23+
/**
24+
* Ant Design Popover component.
25+
*/
26+
export class Popover {
27+
readonly page: Page;
28+
private readonly locator: Locator;
29+
30+
constructor(page: Page, locator?: Locator) {
31+
this.page = page;
32+
this.locator = locator ?? page.locator('.ant-popover-content');
33+
}
34+
35+
get element(): Locator {
36+
return this.locator;
37+
}
38+
39+
async waitForVisible(options?: { timeout?: number }): Promise<void> {
40+
await this.locator.waitFor({ state: 'visible', ...options });
41+
}
42+
43+
async waitForHidden(options?: { timeout?: number }): Promise<void> {
44+
await this.locator.waitFor({ state: 'hidden', ...options });
45+
}
46+
47+
getButton(name: string): Button {
48+
return new Button(
49+
this.page,
50+
this.locator.getByRole('button', { name, exact: true }),
51+
);
52+
}
53+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919

2020
import { Locator, Page } from '@playwright/test';
21+
import { TIMEOUT } from '../../utils/constants';
2122

2223
/**
2324
* Ant Design Select component selectors
@@ -87,7 +88,7 @@ export class Select {
8788
await this.page
8889
.locator(`${SELECT_SELECTORS.DROPDOWN}:not(.ant-select-dropdown-hidden)`)
8990
.last()
90-
.waitFor({ state: 'hidden', timeout: 5000 })
91+
.waitFor({ state: 'hidden', timeout: TIMEOUT.UI_TRANSITION })
9192
.catch(error => {
9293
// Only ignore TimeoutError (dropdown may already be closed); re-throw others
9394
if (!(error instanceof Error) || error.name !== 'TimeoutError') {

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

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,65 @@ import { Locator, Page } from '@playwright/test';
2121

2222
/**
2323
* Tabs component for Ant Design tab navigation.
24+
*
25+
* Expects the locator to point to the `.ant-tabs` wrapper element
26+
* (not the inner tablist) so that `nav` can scope to the outer tab bar
27+
* and exclude nested/inner tabs (e.g. Results / Query history in SQL Lab).
2428
*/
2529
export class Tabs {
2630
readonly page: Page;
2731
private readonly locator: Locator;
2832

2933
constructor(page: Page, locator?: Locator) {
3034
this.page = page;
31-
// Default to the tablist role if no specific locator provided
32-
this.locator = locator ?? page.getByRole('tablist');
35+
this.locator = locator ?? page.locator('.ant-tabs').first();
3336
}
3437

3538
/**
36-
* Gets the tablist element locator
39+
* The root element locator for this tabs component.
3740
*/
3841
get element(): Locator {
3942
return this.locator;
4043
}
4144

4245
/**
43-
* Gets a tab by name, scoped to this tablist's container
46+
* The tab navigation bar for this component.
47+
* Scoped to the first `.ant-tabs-nav` descendant so that queries
48+
* only hit this component's tabs, never nested/inner tab bars.
49+
*/
50+
protected get nav(): Locator {
51+
return this.locator.locator('.ant-tabs-nav').first();
52+
}
53+
54+
/**
55+
* Returns the number of tabs.
56+
* Counts `.ant-tabs-tab` wrappers in the nav bar — one per physical tab,
57+
* regardless of inner role="tab" elements (btn, remove button, etc.).
58+
*/
59+
async getTabCount(): Promise<number> {
60+
return this.nav.locator('.ant-tabs-tab').count();
61+
}
62+
63+
/**
64+
* Returns the text content of all tabs.
65+
*/
66+
async getTabNames(): Promise<string[]> {
67+
return this.nav.locator('.ant-tabs-tab-btn').allTextContents();
68+
}
69+
70+
/**
71+
* Gets a tab button by name, scoped to this component's nav bar.
72+
* Anchored at start (^) with negative lookahead (?!\d) to prevent
73+
* partial matches: "Query" won't match "Query 1", and "Query 1"
74+
* won't match "Query 10". Trailing icon text (e.g. " circle-solid")
75+
* is allowed since (?!\d) permits non-digit suffixes.
4476
* @param tabName - The name/label of the tab
4577
*/
4678
getTab(tabName: string): Locator {
47-
return this.locator.getByRole('tab', { name: tabName });
79+
const escaped = tabName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
80+
return this.nav
81+
.locator('.ant-tabs-tab-btn')
82+
.filter({ hasText: new RegExp(`^${escaped}(?!\\d)`) });
4883
}
4984

5085
/**
@@ -63,6 +98,16 @@ export class Tabs {
6398
return this.page.getByRole('tabpanel', { name: tabName });
6499
}
65100

101+
/**
102+
* Returns the name of the currently active tab.
103+
*/
104+
async getActiveTabName(): Promise<string> {
105+
const text = await this.nav
106+
.locator('.ant-tabs-tab-active .ant-tabs-tab-btn')
107+
.textContent();
108+
return text?.trim() ?? '';
109+
}
110+
66111
/**
67112
* Checks if a tab is selected
68113
* @param tabName - The name/label of the tab

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@
1919

2020
// Core Playwright Components for Superset
2121
export { AceEditor } from './AceEditor';
22+
export { AgGrid } from './AgGrid';
2223
export { Button } from './Button';
2324
export { Checkbox } from './Checkbox';
25+
export { EditableTabs } from './EditableTabs';
2426
export { Form } from './Form';
2527
export { Input } from './Input';
2628
export { Menu } from './Menu';
2729
export { Modal } from './Modal';
30+
export { Popover } from './Popover';
2831
export { Select } from './Select';
2932
export { Table } from './Table';
3033
export { Tabs } from './Tabs';

superset-frontend/playwright/components/modals/EditDatasetModal.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ export class EditDatasetModal extends Modal {
4040
// Use getByRole with specific name to target Edit Dataset dialog
4141
// The dialog has aria-labelledby that resolves to "edit Edit Dataset"
4242
this.specificLocator = page.getByRole('dialog', { name: /edit.*dataset/i });
43-
// Scope tabs to modal's tablist to avoid matching tablists elsewhere on page
44-
this.tabs = new Tabs(page, this.specificLocator.getByRole('tablist'));
43+
// Scope tabs to modal dialog so nav getter finds .ant-tabs-nav as descendant
44+
this.tabs = new Tabs(
45+
page,
46+
this.specificLocator.locator('.ant-tabs').first(),
47+
);
4548
}
4649

4750
/**
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 { Page } from '@playwright/test';
21+
import { Input, Modal } from '../core';
22+
23+
/**
24+
* Save Dataset modal in SQL Lab.
25+
* Appears when clicking "Save dataset" after running a query.
26+
*/
27+
export class SaveDatasetModal extends Modal {
28+
constructor(page: Page) {
29+
super(page, '[data-test="Save or Overwrite Dataset-modal"] .ant-modal');
30+
}
31+
32+
private get nameInput(): Input {
33+
return new Input(
34+
this.page,
35+
this.body.locator('input[placeholder="Dataset name"]'),
36+
);
37+
}
38+
39+
async fillName(name: string): Promise<void> {
40+
await this.nameInput.clear();
41+
await this.nameInput.fill(name);
42+
}
43+
}

0 commit comments

Comments
 (0)