This is an automated email from the ASF dual-hosted git repository.

voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new ec96819966 [ZEPPELIN-6516] Add visualization rendering E2E coverage
ec96819966 is described below

commit ec96819966b6db710a508490c74367f88efa8e26
Author: Minho Jang <[email protected]>
AuthorDate: Sun Sep 13 19:29:28 2026 +0900

    [ZEPPELIN-6516] Add visualization rendering E2E coverage
    
    ### What is this PR for?
    Ths PR adds Playwright E2E coverage for Zeppelin's table and G2-based chart 
visualizations.
    
    The test creates deterministic '%table' output through a notebook paragraph 
and verifies:
    - Exact table headers and cell values
    - Bar chart rendering
    - Pie chart rendering
    - Line chart rendering
    - Area chart rendering
    - Scatter chart rendering
    - Table data preservation after switching between visualization modes
    
    Since G2 renders charts on canvas, checking only that a canvas element is 
visible is not sufficient. The test also inspects the canvas alpha channel and 
verifies that it contains rendered pixels.
    
    A dedicated `NotebookVisualizationPage` Page Object is added to isolate 
visualization selectors, table locators, chart canvas locators, and 
canvas-rendering inspection from the scenario code.
    
    ### Todos
    * [x] Add E2E coverage for table rendering
    * [x] Add E2E coverage for bar chart rendering
    * [x] Add E2E coverage for pie chart rendering
    * [x] Add E2E coverage for line chart rendering
    * [x] Add E2E coverage for area chart rendering
    * [x] Add E2E coverage for scatter chart rendering
    
    ### What type of PR is it?
    Improvement
    
    ### What is the Jira issue?
    * https://issues.apache.org/jira/browse/ZEPPELIN-6516
    
    ### How should this be tested?
    
    Start the Zeppelin backend, then run the focused Chromium E2E test:
    ```bash
    cd zeppelin-web-angular
    npm run e2e:fast -- tests/notebook/paragraph/visualization-rendering.spec.ts
    ```
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    
    Closes #5474 from miinhho/test/visual-charts-table-rendering.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 .../e2e/models/notebook-visualization-page.ts      |  71 +++++++++++
 .../paragraph/visualization-rendering.spec.ts      | 131 +++++++++++++++++++++
 2 files changed, 202 insertions(+)

diff --git a/zeppelin-web-angular/e2e/models/notebook-visualization-page.ts 
b/zeppelin-web-angular/e2e/models/notebook-visualization-page.ts
new file mode 100644
index 0000000000..a223398462
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/notebook-visualization-page.ts
@@ -0,0 +1,71 @@
+/*
+ * Licensed 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 { BasePage } from './base-page';
+
+export class NotebookVisualizationPage extends BasePage {
+  readonly tableMode: Locator;
+  readonly barChartMode: Locator;
+  readonly pieChartMode: Locator;
+  readonly lineChartMode: Locator;
+  readonly areaChartMode: Locator;
+  readonly scatterChartMode: Locator;
+  readonly dataTable: Locator;
+  readonly tableHeaders: Locator;
+  readonly tableCells: Locator;
+  readonly barChartCanvas: Locator;
+  readonly pieChartCanvas: Locator;
+  readonly lineChartCanvas: Locator;
+  readonly areaChartCanvas: Locator;
+  readonly scatterChartCanvas: Locator;
+  private readonly resultDisplay: Locator;
+
+  constructor(page: Page) {
+    super(page);
+    this.resultDisplay = page.locator('zeppelin-notebook-paragraph-result');
+    this.tableMode = 
this.resultDisplay.locator('label.viz-icon:has(.anticon-table)');
+    this.barChartMode = 
this.resultDisplay.locator('label.viz-icon:has(.anticon-bar-chart)');
+    this.pieChartMode = 
this.resultDisplay.locator('label.viz-icon:has(.anticon-pie-chart)');
+    this.lineChartMode = 
this.resultDisplay.locator('label.viz-icon:has(.anticon-line-chart)');
+    this.areaChartMode = 
this.resultDisplay.locator('label.viz-icon:has(.anticon-area-chart)');
+    this.scatterChartMode = 
this.resultDisplay.locator('label.viz-icon:has(.anticon-dot-chart)');
+    this.dataTable = this.resultDisplay.getByRole('table');
+    this.tableHeaders = this.dataTable.locator('thead th');
+    this.tableCells = this.dataTable.locator('tbody tr.ant-table-row td');
+    this.barChartCanvas = 
this.resultDisplay.locator('zeppelin-bar-chart-visualization canvas');
+    this.pieChartCanvas = 
this.resultDisplay.locator('zeppelin-pie-chart-visualization canvas');
+    this.lineChartCanvas = 
this.resultDisplay.locator('zeppelin-line-chart-visualization canvas');
+    this.areaChartCanvas = 
this.resultDisplay.locator('zeppelin-area-chart-visualization canvas');
+    this.scatterChartCanvas = 
this.resultDisplay.locator('zeppelin-scatter-chart-visualization canvas');
+  }
+
+  async renderedPixelCount(canvas: Locator): Promise<number> {
+    return canvas.evaluate((element: HTMLCanvasElement) => {
+      const context = element.getContext('2d');
+      if (!context || element.width === 0 || element.height === 0) {
+        return 0;
+      }
+
+      const pixels = context.getImageData(0, 0, element.width, 
element.height).data;
+      let count = 0;
+      for (let index = 3; index < pixels.length; index += 4) {
+        if (pixels[index] > 0) {
+          count += 1;
+        }
+      }
+      return count;
+    });
+  }
+}
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/paragraph/visualization-rendering.spec.ts
 
b/zeppelin-web-angular/e2e/tests/notebook/paragraph/visualization-rendering.spec.ts
new file mode 100644
index 0000000000..dd96106086
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/notebook/paragraph/visualization-rendering.spec.ts
@@ -0,0 +1,131 @@
+/*
+ * Licensed 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 { expect, Locator, test } from '@playwright/test';
+import { NotebookParagraphPage } from 'e2e/models/notebook-paragraph-page';
+import { NotebookVisualizationPage } from 
'e2e/models/notebook-visualization-page';
+import {
+  addPageAnnotation,
+  addPageAnnotationBeforeEach,
+  createTestNotebook,
+  PAGES,
+  performLoginIfRequired,
+  setParagraphText,
+  waitForZeppelinReady
+} from '../../../utils';
+
+const TABLE_PARAGRAPH = `%sh
+printf '%%table 
city\\tsales\\tcost\\nSeoul\\t30\\t12\\nBusan\\t20\\t8\\nIncheon\\t10\\t5\\n'`;
+const TABLE_HEADERS = ['city', 'sales', 'cost'];
+const TABLE_CELLS = ['Seoul', '30', '12', 'Busan', '20', '8', 'Incheon', '10', 
'5'];
+
+test.describe('Notebook Visualization Rendering', () => {
+  addPageAnnotationBeforeEach(PAGES.VISUALIZATIONS.TABLE);
+
+  let paragraphPage: NotebookParagraphPage;
+  let visualizationPage: NotebookVisualizationPage;
+
+  test.beforeEach(async ({ page }) => {
+    await test.step('Given a notebook paragraph with deterministic table 
output', async () => {
+      await page.goto('/#/');
+      await waitForZeppelinReady(page);
+      await performLoginIfRequired(page);
+
+      const { noteId, paragraphId } = await createTestNotebook(page);
+      await setParagraphText(page, noteId, paragraphId, TABLE_PARAGRAPH);
+
+      paragraphPage = new NotebookParagraphPage(page);
+      visualizationPage = new NotebookVisualizationPage(page);
+      await page.goto(`/#/notebook/${noteId}`);
+      await expect(paragraphPage.paragraphContainer).toBeVisible({ timeout: 
30000 });
+
+      await paragraphPage.runParagraph();
+      await expect(visualizationPage.dataTable).toBeVisible({ timeout: 30000 
});
+    });
+  });
+
+  test('renders the exact table headers and rows', async () => {
+    await test.step('Then the table presents every output field and value', 
async () => {
+      await 
expect(visualizationPage.tableMode.locator('input[type="radio"]')).toBeChecked();
+      await expect(visualizationPage.tableHeaders).toHaveText(TABLE_HEADERS);
+      await expect(visualizationPage.tableCells).toHaveText(TABLE_CELLS);
+    });
+  });
+
+  test('renders every G2 chart and preserves table data after switching back', 
async ({}, testInfo) => {
+    const charts: Array<{ name: string; page: string; mode: Locator; canvas: 
Locator }> = [
+      {
+        name: 'Bar Chart',
+        page: PAGES.VISUALIZATIONS.BAR_CHART,
+        mode: visualizationPage.barChartMode,
+        canvas: visualizationPage.barChartCanvas
+      },
+      {
+        name: 'Pie Chart',
+        page: PAGES.VISUALIZATIONS.PIE_CHART,
+        mode: visualizationPage.pieChartMode,
+        canvas: visualizationPage.pieChartCanvas
+      },
+      {
+        name: 'Line Chart',
+        page: PAGES.VISUALIZATIONS.LINE_CHART,
+        mode: visualizationPage.lineChartMode,
+        canvas: visualizationPage.lineChartCanvas
+      },
+      {
+        name: 'Area Chart',
+        page: PAGES.VISUALIZATIONS.AREA_CHART,
+        mode: visualizationPage.areaChartMode,
+        canvas: visualizationPage.areaChartCanvas
+      },
+      {
+        name: 'Scatter Chart',
+        page: PAGES.VISUALIZATIONS.SCATTER_CHART,
+        mode: visualizationPage.scatterChartMode,
+        canvas: visualizationPage.scatterChartCanvas
+      }
+    ];
+
+    for (const chart of charts) {
+      await test.step(`When selecting ${chart.name}`, async () => {
+        addPageAnnotation(chart.page, testInfo);
+        await expect(async () => {
+          await chart.mode.click();
+          await 
expect(chart.mode.locator('input[type="radio"]')).toBeChecked({ timeout: 1000 
});
+        }).toPass({ timeout: 10000 });
+      });
+
+      await test.step(`Then ${chart.name} draws visible canvas pixels`, async 
() => {
+        await expect(chart.canvas).toBeVisible();
+        await expect
+          .poll(() => visualizationPage.renderedPixelCount(chart.canvas), {
+            message: `${chart.name} canvas should contain rendered pixels`
+          })
+          .toBeGreaterThan(100);
+      });
+    }
+
+    await test.step('When switching from the final chart back to Table', async 
() => {
+      await expect(async () => {
+        await visualizationPage.tableMode.click();
+        await 
expect(visualizationPage.tableMode.locator('input[type="radio"]')).toBeChecked({
 timeout: 1000 });
+      }).toPass({ timeout: 10000 });
+    });
+
+    await test.step('Then the original table data remains intact', async () => 
{
+      await expect(visualizationPage.dataTable).toBeVisible();
+      await expect(visualizationPage.tableCells).toHaveText(TABLE_CELLS);
+    });
+  });
+});

Reply via email to