sadpandajoe commented on code in PR #41438:
URL: https://github.com/apache/superset/pull/41438#discussion_r3715374253
##########
superset-frontend/playwright/pages/DashboardPage.ts:
##########
@@ -178,4 +255,192 @@ export class DashboardPage {
await menu.selectSubmenuItem('Download', optionText);
return downloadPromise;
}
+
+ /**
+ * Enter dashboard edit mode and wait for the builder side pane to appear.
+ */
+ async enterEditMode(): Promise<void> {
+ const editButton = new Button(
+ this.page,
+ DashboardPage.SELECTORS.EDIT_BUTTON,
+ );
+ await editButton.click();
+ await this.page
+ .locator(DashboardPage.SELECTORS.BUILDER_PANE)
+ .waitFor({ state: 'visible' });
+ }
+
+ /**
+ * The builder side pane's tab bar (Charts / Layout elements).
+ */
+
+ /**
+ * Switch the builder side pane to one of its tabs.
+ * @param tab - 'Charts' (existing slices) or 'Layout elements' (new
components)
+ */
+ private async openBuilderTab(tab: BuilderTab): Promise<void> {
+ // Scoped to `.ant-tabs` because that is the root the shared Tabs component
+ // expects.
+ const builderTabs = new Tabs(
+ this.page,
+ this.page
+ .locator(`${DashboardPage.SELECTORS.BUILDER_PANE} .ant-tabs`)
+ .first(),
+ );
+ await builderTabs.clickTab(tab);
+ }
+
+ /**
+ * Locator for chart-holder components currently placed on the grid.
+ * Markdown components are chart holders too — use
+ * {@link getMarkdownEditors} when the assertion must exclude them.
+ */
+ getChartHolders(): Locator {
+ return this.page.locator(DashboardPage.SELECTORS.CHART_HOLDER);
+ }
+
+ /**
+ * Drag an existing chart from the Charts pane onto the dashboard grid.
+ * Requires edit mode to be active.
+ * @param sliceName - The slice name to search for and drag
+ */
+ async addChartByName(sliceName: string): Promise<void> {
+ await this.openBuilderTab('Charts');
+ const search = new Input(this.page, DashboardPage.SELECTORS.CHARTS_SEARCH);
+ await search.fill(sliceName);
+ const card = this.page
+ .locator(DashboardPage.SELECTORS.CHART_CARD)
+ .filter({ hasText: sliceName })
+ .first();
+ await card.waitFor({ state: 'visible' });
+ await html5DragAndDrop(this.page, card, this.dropTarget());
+ }
+
+ /**
+ * Drag a new Layout element (by its label) onto the dashboard grid.
+ * Requires edit mode to be active.
+ * @param label - The new-component label, e.g. 'Text / Markdown'
+ */
+ async addLayoutElement(label: LayoutElementLabel): Promise<void> {
+ await this.openBuilderTab('Layout elements');
+ const source = this.page
+ .locator(DashboardPage.SELECTORS.NEW_COMPONENT)
+ .filter({ hasText: label })
+ .first();
+ await source.waitFor({ state: 'visible' });
+ await html5DragAndDrop(this.page, source, this.dropTarget());
+ }
+
+ /**
+ * The grid's empty drop target, which the grid renders while in edit mode.
+ *
+ * Only resolves while the grid is still empty. Dropping a second component
+ * needs a target relative to the already-placed one, not this.
+ */
+ private dropTarget(): Locator {
+ return this.page.locator(DashboardPage.SELECTORS.EMPTY_DROPTARGET).first();
+ }
+
+ /**
+ * Hover the first placed chart-holder and click its delete button (edit
mode).
+ */
+ async deleteChartHolder(): Promise<void> {
+ const holder = this.getChartHolders().first();
+ await holder.hover();
+ const deleteButton = new Button(
+ this.page,
+ holder.locator(DashboardPage.SELECTORS.DELETE_COMPONENT),
+ );
+ await deleteButton.click();
+ }
+
+ /**
+ * Locator for markdown editor components on the grid.
+ */
+ getMarkdownEditors(): Locator {
+ return this.page.locator(DashboardPage.SELECTORS.MARKDOWN_EDITOR);
+ }
+
+ /**
+ * The rendered ace document inside a markdown component. Present only once
+ * the component has entered its editing state.
+ *
+ * Exposed as a locator rather than routed through the `AceEditor` component:
+ * that component reads and writes through `ace.edit(...)` in page context,
+ * which both bypasses the real keystroke path under test and gives up
+ * web-first retries on assertions.
+ *
+ * @param markdownEditor - A locator from {@link getMarkdownEditors}
+ */
+ getMarkdownAceContent(markdownEditor: Locator): Locator {
+ return markdownEditor.locator(DashboardPage.SELECTORS.ACE_CONTENT);
+ }
+
+ /**
+ * Ace's hidden textarea inside a markdown component — the element that
+ * receives keystrokes.
+ *
+ * @param markdownEditor - A locator from {@link getMarkdownEditors}
+ */
+ getMarkdownAceInput(markdownEditor: Locator): Locator {
+ return markdownEditor.locator(DashboardPage.SELECTORS.ACE_TEXT_INPUT);
+ }
+
+ /**
+ * Click the dashboard title, moving focus off whichever grid component holds
+ * it. Committing a markdown edit needs a click on some other element, and
the
+ * title is the one that is always present regardless of what is on the grid.
+ *
+ * In edit mode the click focuses the title's input. That is a state change,
+ * not a no-op — but it edits nothing on its own, so it leaves the component
+ * under test untouched.
+ */
+ async blurToDashboardTitle(): Promise<void> {
+ await this.page
+ .locator(DashboardPage.SELECTORS.EDITABLE_TITLE)
+ .first()
+ .click();
+ }
+
+ /**
+ * Drag a grid component's bottom resize handle down by `deltaY` pixels.
+ * Requires edit mode. Uses the mouse because the resize handle is driven by
+ * `react-resizable`, which tracks real pointer movement.
+ *
+ * @param component - The grid component to resize
+ * @param deltaY - Pixels to drag downwards (positive grows the component)
+ * @returns The component's height before and after the drag
+ */
+ async resizeComponent(
+ component: Locator,
+ deltaY: number,
+ ): Promise<{ heightBefore: number; heightAfter: number }> {
+ const boxBefore = await component.boundingBox();
+ if (!boxBefore) {
+ throw new Error('Cannot resize a component that is not visible');
+ }
+
+ const handle = component
+ .locator(DashboardPage.SELECTORS.RESIZE_HANDLE_BOTTOM)
+ .last();
+ const handleBox = await handle.boundingBox();
+ if (!handleBox) {
+ throw new Error('Resize handle is not visible');
+ }
+
+ const startX = handleBox.x + handleBox.width / 2;
+ const startY = handleBox.y + handleBox.height / 2;
+ await this.page.mouse.move(startX, startY);
+ await this.page.mouse.down();
+ // Multiple steps so react-resizable sees a drag rather than a teleport.
+ await this.page.mouse.move(startX, startY + deltaY, { steps: 10 });
+ await this.page.mouse.up();
+
+ const boxAfter = await component.boundingBox();
+ if (!boxAfter) {
Review Comment:
Fixed in `15e39f8b2c` — `resizeComponent` now polls until the component
height changes before reading the final bounding box. The targeted
pre-commit/typecheck suite and markdown resize Playwright test pass.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]