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);
+ });
+ });
+});