Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0c95457
test(playwright): add export single dataset E2E test
sadpandajoe Dec 16, 2025
0a8a24d
test(playwright): add bulk export datasets E2E test
sadpandajoe Dec 16, 2025
ba0f01b
test(playwright): add create dataset wizard E2E test
sadpandajoe Dec 16, 2025
faf0b9d
test(playwright): add edit dataset E2E test
sadpandajoe Dec 16, 2025
3954bbd
fix(playwright): address PR review comments
sadpandajoe Dec 16, 2025
8bdd1d9
fix(playwright): use specific selector for add dataset button
sadpandajoe Dec 16, 2025
4198ff3
fix(playwright): fix Select.clickOption and export test reliability
sadpandajoe Dec 17, 2025
470938d
refactor(playwright): address review feedback on tests
sadpandajoe Dec 17, 2025
427cf75
fix(playwright): use selector-based approach for Select component
sadpandajoe Dec 17, 2025
fac22b4
fix(playwright): fix failing dataset tests with proper selectors
sadpandajoe Dec 17, 2025
3aeb24e
fix(playwright): improve component stability and selector reliability
sadpandajoe Dec 18, 2025
426772d
fix(playwright): add duplicateDataset API helper and export dependencies
sadpandajoe Dec 18, 2025
5c0ea58
fix(playwright): address review feedback on types and test structure
sadpandajoe Dec 18, 2025
7cd0913
fix(playwright): handle API response variations in duplicate dataset
sadpandajoe Dec 18, 2025
9d3127e
fix(playwright): validate duplicate dataset id and handle response shape
sadpandajoe Dec 18, 2025
a42cc22
fix(playwright): improve type safety, download robustness, and app pr…
sadpandajoe Dec 18, 2025
f807ca4
fix(playwright): use API response interception for export test
sadpandajoe Dec 18, 2025
50581a8
fix(playwright): refactor edit dataset test to use name field
sadpandajoe Jan 14, 2026
e8501d9
feat(playwright): add create dataset wizard test with Google Sheets
sadpandajoe Jan 15, 2026
057cfe1
fix(playwright): improve Select.type() robustness and DRY export vali…
sadpandajoe Jan 15, 2026
8a7a06f
fix(playwright): improve CreateDatasetPage dropdown trigger locator
sadpandajoe Jan 23, 2026
ab506b6
fix(playwright): improve security and error handling in test infrastr…
sadpandajoe Jan 26, 2026
381d817
chore: update jsdocs and comments
sadpandajoe Jan 26, 2026
d7123ce
refactor(playwright): extract BulkSelect component for reusability
sadpandajoe Jan 27, 2026
881e60f
refactor(playwright): move BulkSelect to components/ListView
sadpandajoe Jan 27, 2026
82e85db
fix(playwright): consolidate clickOk API and improve test robustness
sadpandajoe Jan 27, 2026
d11046e
refactor(playwright): extract timeout constants for clarity
sadpandajoe Jan 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions superset-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions superset-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@
"@types/rison": "0.1.0",
"@types/sinon": "^17.0.3",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"babel-jest": "^30.0.2",
Expand Down Expand Up @@ -361,6 +362,7 @@
"tscw-config": "^1.1.2",
"tsx": "^4.21.0",
"typescript": "5.4.5",
"unzipper": "^0.12.3",
"vm-browserify": "^1.1.2",
"wait-on": "^9.0.3",
"webpack": "^5.104.1",
Expand Down
24 changes: 18 additions & 6 deletions superset-frontend/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export default defineConfig({

viewport: { width: 1280, height: 1024 },

// Accept downloads without prompts (needed for export tests)
acceptDownloads: true,

// Screenshots and videos on failure
screenshot: 'only-on-failure',
video: 'retain-on-failure',
Expand Down Expand Up @@ -117,10 +120,19 @@ export default defineConfig({
// Web server setup - disabled in CI (Flask started separately in workflow)
webServer: process.env.CI
? undefined
: {
command: 'curl -f http://localhost:8088/health',
url: 'http://localhost:8088/health',
reuseExistingServer: true,
timeout: 5000,
},
: (() => {
// Support custom base URL (e.g., http://localhost:9012/app/prefix/)
const baseUrl =
process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8088';
// Extract origin (scheme + host + port) for health check
// Health endpoint is always at /health regardless of app prefix
const healthUrl = new URL('/health', new URL(baseUrl).origin).href;
return {
// Quote URL to prevent shell injection via PLAYWRIGHT_BASE_URL
command: `curl -f '${healthUrl}'`,
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shell injection vulnerability: The healthUrl variable is constructed from process.env.PLAYWRIGHT_BASE_URL and directly interpolated into a shell command with single quotes. While single quotes prevent most shell injection, a malicious URL containing single quotes could break out of the quoted string. Use a safer approach such as using Node.js's child_process with array arguments or properly escaping the URL.

Copilot uses AI. Check for mistakes.
url: healthUrl,
reuseExistingServer: true,
Comment on lines +130 to +134
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shell injection risk in webServer command

The curl command uses single quotes around the healthUrl, but if PLAYWRIGHT_BASE_URL contains single quotes, it could lead to shell injection. Switching to double quotes prevents this since URLs typically don't contain double quotes.

Code suggestion
Check the AI-generated fix before applying
Suggested change
return {
// Quote URL to prevent shell injection via PLAYWRIGHT_BASE_URL
command: `curl -f '${healthUrl}'`,
url: healthUrl,
reuseExistingServer: true,
return {
// Quote URL to prevent shell injection via PLAYWRIGHT_BASE_URL
command: `curl -f "${healthUrl}"`,
url: healthUrl,
reuseExistingServer: true,

Code Review Run #819970


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

timeout: 5000,
};
})(),
});
116 changes: 116 additions & 0 deletions superset-frontend/playwright/components/ListView/BulkSelect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* 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';
import { Button, Checkbox, Table } from '../core';

const BULK_SELECT_SELECTORS = {
CONTROLS: '[data-test="bulk-select-controls"]',
ACTION: '[data-test="bulk-select-action"]',
} as const;

/**
* BulkSelect component for Superset ListView bulk operations.
* Provides a reusable interface for bulk selection and actions across list pages.
*
* @example
* const bulkSelect = new BulkSelect(page, table);
* await bulkSelect.enable();
* await bulkSelect.selectRow('my-dataset');
* await bulkSelect.selectRow('another-dataset');
* await bulkSelect.clickAction('Delete');
*/
export class BulkSelect {
private readonly page: Page;
private readonly table: Table;

constructor(page: Page, table: Table) {
this.page = page;
this.table = table;
}

/**
* Gets the "Bulk select" toggle button
*/
getToggleButton(): Button {
return new Button(
this.page,
this.page.getByRole('button', { name: 'Bulk select' }),
);
}

/**
* Enables bulk selection mode by clicking the toggle button
*/
async enable(): Promise<void> {
await this.getToggleButton().click();
}

/**
* Gets the checkbox for a row by name
* @param rowName - The name/text identifying the row
*/
getRowCheckbox(rowName: string): Checkbox {
const row = this.table.getRow(rowName);
return new Checkbox(this.page, row.getByRole('checkbox'));
}

/**
* Selects a row's checkbox in bulk select mode
* @param rowName - The name/text identifying the row to select
*/
async selectRow(rowName: string): Promise<void> {
await this.getRowCheckbox(rowName).check();
}

/**
* Deselects a row's checkbox in bulk select mode
* @param rowName - The name/text identifying the row to deselect
*/
async deselectRow(rowName: string): Promise<void> {
await this.getRowCheckbox(rowName).uncheck();
}

/**
* Gets the bulk select controls container locator (for assertions)
*/
getControls(): Locator {
return this.page.locator(BULK_SELECT_SELECTORS.CONTROLS);
}

/**
* Gets a bulk action button by name
* @param actionName - The name of the bulk action (e.g., "Export", "Delete")
*/
getActionButton(actionName: string): Button {
const controls = this.getControls();
return new Button(
this.page,
controls.locator(BULK_SELECT_SELECTORS.ACTION, { hasText: actionName }),
);
}

/**
* Clicks a bulk action button by name (e.g., "Export", "Delete")
* @param actionName - The name of the bulk action to click
*/
async clickAction(actionName: string): Promise<void> {
await this.getActionButton(actionName).click();
}
}
21 changes: 21 additions & 0 deletions superset-frontend/playwright/components/ListView/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* 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.
*/

// ListView-specific Playwright Components for Superset
export { BulkSelect } from './BulkSelect';
152 changes: 152 additions & 0 deletions superset-frontend/playwright/components/core/AceEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* 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';

const ACE_EDITOR_SELECTORS = {
TEXT_INPUT: '.ace_text-input',
TEXT_LAYER: '.ace_text-layer',
CONTENT: '.ace_content',
SCROLLER: '.ace_scroller',
} as const;

/**
* AceEditor component for interacting with Ace Editor instances in Playwright.
* Uses the ace editor API directly for reliable text manipulation.
*/
export class AceEditor {
readonly page: Page;
private readonly selector: string;
private readonly locator: Locator;

constructor(page: Page, selector: string) {
this.page = page;
this.selector = selector;
this.locator = page.locator(selector);
}

/**
* Gets the editor element locator
*/
get element(): Locator {
return this.locator;
}

/**
* Waits for the ace editor to be fully loaded and ready for interaction.
*/
async waitForReady(): Promise<void> {
await this.locator.waitFor({ state: 'visible' });
await this.locator.locator(ACE_EDITOR_SELECTORS.CONTENT).waitFor();
await this.locator.locator(ACE_EDITOR_SELECTORS.TEXT_LAYER).waitFor();
}

/**
* Sets text in the ace editor using the ace API.
* @param text - The text to set
*/
async setText(text: string): Promise<void> {
await this.waitForReady();
const editorId = this.extractEditorId();
await this.page.evaluate(
({ id, value }) => {
const editor = (window as any).ace.edit(id);
editor.setValue(value, 1);
editor.session.getUndoManager().reset();
},
{ id: editorId, value: text },
);
}

/**
* Gets the text content from the ace editor.
* @returns The text content
*/
async getText(): Promise<string> {
await this.waitForReady();
const editorId = this.extractEditorId();
return this.page.evaluate(id => {
const editor = (window as any).ace.edit(id);
return editor.getValue();
}, editorId);
}

/**
* Clears the text in the ace editor.
*/
async clear(): Promise<void> {
await this.setText('');
}

/**
* Appends text to the existing content in the ace editor.
* @param text - The text to append
*/
async appendText(text: string): Promise<void> {
await this.waitForReady();
const editorId = this.extractEditorId();
await this.page.evaluate(
({ id, value }) => {
const editor = (window as any).ace.edit(id);
const currentText = editor.getValue();
const newText = currentText + (currentText ? '\n' : '') + value;
editor.setValue(newText, 1);
},
{ id: editorId, value: text },
);
}

/**
* Focuses the ace editor.
*/
async focus(): Promise<void> {
await this.waitForReady();
const editorId = this.extractEditorId();
await this.page.evaluate(id => {
const editor = (window as any).ace.edit(id);
editor.focus();
}, editorId);
}

/**
* Checks if the editor is visible.
*/
async isVisible(): Promise<boolean> {
return this.locator.isVisible();
}

/**
* Extracts the editor ID from the selector.
* Handles selectors like '#ace-editor' or '[id="ace-editor"]'
*/
private extractEditorId(): string {
// Handle #id format
if (this.selector.startsWith('#')) {
return this.selector.slice(1);
}
// Handle [id="..."] format
const idMatch = this.selector.match(/id="([^"]+)"/);
if (idMatch) {
return idMatch[1];
}
// Handle [data-test="..."] format - use the element's actual id
// This requires getting it from the DOM
return this.selector.replace(/[#[\]]/g, '');
}
}
Loading
Loading