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 50b0f4071a [ZEPPELIN-6650] Add unit tests for the React result 
renderers
50b0f4071a is described below

commit 50b0f4071af527b4ebd840fa13431b520fe451a5
Author: κΉ€μ˜ˆλ‚˜ <[email protected]>
AuthorDate: Fri Sep 4 21:51:58 2026 +0900

    [ZEPPELIN-6650] Add unit tests for the React result renderers
    
    ### What is this PR for?
    
    Unit tests for the two files that decide what a result looks like before it 
is drawn, following the conventions ZEPPELIN-6648 added in 
`projects/zeppelin-react/AGENTS.md`.
    
    **`SingleResultRenderer`** gets one spec per `DatasetType` arm, asserting 
what the user ends up seeing rather than which component was chosen: markup for 
HTML, a base64 png for IMG, the unsupported notice for ANGULAR, and nothing at 
all for a type with no renderer, since drawing something misleading would be 
worse than drawing nothing. The TABLE arm asserts only that it was taken. What 
the visualization does with the data is its own concern, and its display-mode 
state is the defect `A [...]
    
    **`HTMLRenderer`** is the reason that file assigns `innerHTML` by hand. A 
script parsed out of `innerHTML` is inert for good, so the component builds a 
fresh element and puts it in the old one's place. jsdom never runs scripts, and 
neither `runScripts` nor the environment options change that here, so the 
execution itself is out of reach and belongs in e2e. What is observable is the 
swap, and that is what these specs pin: attributes and body carried over, 
`async` forced off so a librar [...]
    
    Two specs were written and then dropped because they could not fail, which 
`AGENTS.md` names as the thing lint is there to catch:
    
    * A carriage-return spec at the `SingleResultRenderer` level. 
`ansi-to-react` already collapses `\r` the same way 
`checkAndReplaceCarriageReturn` does, so removing the call leaves the rendered 
text identical. `textUtils.spec.ts` covers the helper itself.
    * An unmount spec for `HTMLRenderer`. React removes the container it owns 
whether or not the effect cleans up, so the assertion holds either way.
    
    The `matchMedia` stub in `test-setup.ts` is what lets the TABLE arm render 
at all: antd's responsive observer calls it and jsdom implements none. The 
identical block is in the ZEPPELIN-6630 branch (#5436), so whichever of the two 
lands second drops it in rebase. Flagging it rather than hiding it in a spec 
file, since it is package infrastructure and the next spec touching an antd 
component needs it too.
    
    Two things noticed while writing these, both out of scope here and neither 
touched: `HTMLRenderer` imports `highlight.js`, which this package does not 
declare (it resolves through the shell's `node_modules` and webpack's 
`resolve.modules` fallback), and that dynamic import has no `.catch()`, so a 
failed chunk becomes an unhandled rejection.
    
    ### What type of PR is it?
    
    Improvement
    
    ### Todos
    
    None
    
    ### What is the Jira issue?
    
    https://issues.apache.org/jira/browse/ZEPPELIN-6650
    
    ### How should this be tested?
    
    `npm test` from `projects/zeppelin-react`, green at 59. `npm test -- 
--coverage` for the figures above; note that a file at 100% on every metric is 
omitted from the printed table, so `SingleResultRenderer` shows up only in 
`coverage/lcov.info`.
    
    Every assertion was checked by breaking what it covers, twelve in all. 
Removing the script-replacement block, the attribute copy, `async = false`, the 
highlight block, or changing the effect's dependency array or `innerHTML` to 
`textContent` each fail the HTMLRenderer specs. Misrouting the HTML or TABLE 
arm, returning `null` for ANGULAR, returning an element from the default arm, 
reading `config?.[0]` instead of `config?.[index]`, and changing the image mime 
type each fail exactly one [...]
    
    Also run: `npm run test:shell` (29, unaffected), `npm run build` for the 
remote, `tsc --noEmit`, lint, prettier, and RAT.
    
    This package's suite still does not run in pull-request CI (ZEPPELIN-6566), 
so the assertions here are checked locally only. The lint rules do run.
    
    ### Screenshots (if appropriate)
    
    No, this adds tests only.
    
    ### Questions:
    
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    
    Closes #5440 from kimyenac/ZEPPELIN-6650.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 .../projects/zeppelin-react/package-lock.json      | 20 +++++
 .../projects/zeppelin-react/package.json           |  2 +
 .../HTMLRenderer.failedHighlight.spec.tsx          | 33 +++++++++
 .../src/components/renderers/HTMLRenderer.spec.tsx | 84 +++++++++++++++++++++
 .../src/components/renderers/HTMLRenderer.tsx      |  9 ++-
 .../src/templates/SingleResultRenderer.spec.tsx    | 85 ++++++++++++++++++++++
 6 files changed, 230 insertions(+), 3 deletions(-)

diff --git a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json 
b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
index 0cc55535ae..c988c10777 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
+++ b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
@@ -15,6 +15,7 @@
         "chart.js": "^4.5.1",
         "date-fns": "^3.6.0",
         "file-saver": "2.0.5",
+        "highlight.js": "^9.15.8",
         "react": "18.3.1",
         "react-dom": "18.3.1",
         "rxjs": "^7.8.0",
@@ -25,6 +26,7 @@
         "@testing-library/dom": "10.4.1",
         "@testing-library/react": "16.3.2",
         "@types/file-saver": "2.0.7",
+        "@types/highlight.js": "^9.12.3",
         "@types/node": "22.19.19",
         "@types/react": "18.3.26",
         "@types/react-dom": "18.3.7",
@@ -1980,6 +1982,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/highlight.js": {
+      "version": "9.12.4",
+      "resolved": 
"https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.4.tgz";,
+      "integrity": 
"sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/html-minifier-terser": {
       "version": "6.1.0",
       "resolved": 
"https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz";,
@@ -5519,6 +5528,17 @@
         "he": "bin/he"
       }
     },
+    "node_modules/highlight.js": {
+      "version": "9.18.5",
+      "resolved": 
"https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.5.tgz";,
+      "integrity": 
"sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==",
+      "deprecated": "Support has ended for 9.x series. Upgrade to @latest",
+      "hasInstallScript": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/html-encoding-sniffer": {
       "version": "6.0.0",
       "resolved": 
"https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz";,
diff --git a/zeppelin-web-angular/projects/zeppelin-react/package.json 
b/zeppelin-web-angular/projects/zeppelin-react/package.json
index 1d99fda253..c6441af70b 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/package.json
+++ b/zeppelin-web-angular/projects/zeppelin-react/package.json
@@ -17,6 +17,7 @@
     "@ant-design/icons": "5.4.0",
     "@zeppelin/sdk": "file:../zeppelin-sdk",
     "ansi-to-react": "6.2.6",
+    "highlight.js": "^9.15.8",
     "antd": "5.21.0",
     "chart.js": "^4.5.1",
     "date-fns": "^3.6.0",
@@ -31,6 +32,7 @@
     "@testing-library/dom": "10.4.1",
     "@testing-library/react": "16.3.2",
     "@types/file-saver": "2.0.7",
+    "@types/highlight.js": "^9.12.3",
     "@types/node": "22.19.19",
     "@types/react": "18.3.26",
     "@types/react-dom": "18.3.7",
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.failedHighlight.spec.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.failedHighlight.spec.tsx
new file mode 100644
index 0000000000..77984620d6
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.failedHighlight.spec.tsx
@@ -0,0 +1,33 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import { HTMLRenderer } from './HTMLRenderer';
+
+// A file of its own: the mock is hoisted over the whole module, so it cannot
+// share one with the specs that need highlighting to work.
+vi.mock('highlight.js', () => {
+  throw new Error('chunk load failed');
+});
+
+describe('HTMLRenderer when the highlight chunk fails', () => {
+  it('leaves the code block unhighlighted instead of raising an unhandled 
rejection', async () => {
+    const { container } = render(<HTMLRenderer html="<pre><code>const x = 
1;</code></pre>" />);
+
+    // Give the rejected import a turn to settle; nothing must escape it.
+    await vi.waitFor(() => expect(container.querySelector('pre 
code')).not.toBeNull());
+
+    expect(container.querySelector('pre 
code')!.classList.contains('hljs')).toBe(false);
+    expect(container.textContent).toContain('const x = 1;');
+  });
+});
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.spec.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.spec.tsx
new file mode 100644
index 0000000000..0911a9f15b
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.spec.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 { render, screen, waitFor } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import { HTMLRenderer } from './HTMLRenderer';
+
+// jsdom never runs scripts, so the execution this component exists for is out 
of reach here and belongs in e2e.
+// What these specs pin is the fidelity of the swap:
+// attributes, body and position survive it. They do not prove the swap 
happened,
+// because a script parsed out of innerHTML already carries all three.
+// Only the async assertion below distinguishes a rebuilt script from an inert 
one.
+describe('HTMLRenderer', () => {
+  it('renders the markup it is given', () => {
+    render(<HTMLRenderer html="<p>rendered output</p>" />);
+
+    expect(screen.getByText('rendered output')).toBeTruthy();
+  });
+
+  it('carries the original attributes onto the replacement script', () => {
+    const { container } = render(
+      <HTMLRenderer html='<script type="text/javascript" data-mark="kept" 
src="lib.js"></script>' />
+    );
+
+    const script = container.querySelector('script')!;
+    expect(script.getAttribute('type')).toBe('text/javascript');
+    expect(script.getAttribute('data-mark')).toBe('kept');
+    expect(script.getAttribute('src')).toBe('lib.js');
+  });
+
+  it('keeps the script body so the replacement has something to run', () => {
+    const { container } = render(<HTMLRenderer html="<script>window.answer = 
42;</script>" />);
+
+    expect(container.querySelector('script')!.textContent).toBe('window.answer 
= 42;');
+  });
+
+  it('forces async off even when the source markup asked for it', () => {
+    // A library and the code using it must not arrive out of order.
+    const { container } = render(<HTMLRenderer html='<script async 
src="lib.js"></script>' />);
+
+    expect(container.querySelector('script')!.async).toBe(false);
+  });
+
+  it('leaves each script where it was among the surrounding markup', () => {
+    const { container } = render(
+      <HTMLRenderer html='<p>before</p><script 
id="one"></script><p>between</p><script id="two"></script>' />
+    );
+
+    const ids = Array.from(container.querySelectorAll('.inner-html > 
*')).map(node => node.id || node.tagName);
+    expect(ids).toEqual(['P', 'one', 'P', 'two']);
+  });
+
+  it('highlights a code block, matching what the Angular renderer does', async 
() => {
+    const { container } = render(<HTMLRenderer html="<pre><code>const x = 
1;</code></pre>" />);
+
+    // highlight.js arrives through a dynamic import, so the class lands a tick
+    // later. Which language it guesses is its own business and not pinned 
here.
+    await waitFor(() => expect(container.querySelector('pre 
code')!.classList.contains('hljs')).toBe(true));
+  });
+
+  it('replaces the previous output when the html changes', () => {
+    const { rerender } = render(<HTMLRenderer html="<p>first</p>" />);
+
+    rerender(<HTMLRenderer html="<p>second</p>" />);
+
+    expect(screen.queryByText('first')).toBeNull();
+    expect(screen.getByText('second')).toBeTruthy();
+  });
+
+  it('renders nothing visible for empty html', () => {
+    const { container } = render(<HTMLRenderer html="" />);
+
+    expect(container.textContent).toBe('');
+  });
+});
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.tsx
index 45fbeec9d4..93ba5b5223 100644
--- 
a/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.tsx
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.tsx
@@ -31,9 +31,12 @@ export const HTMLRenderer = ({ html }: HTMLRendererProps) => 
{
       // Highlight code blocks (matches Angular: result.component.ts 
renderHTML)
       const codeEle = container.querySelector('pre code');
       if (codeEle) {
-        import('highlight.js').then(({ default: hljs }) => {
-          hljs.highlightBlock(codeEle as HTMLElement);
-        });
+        import('highlight.js')
+          .then(({ default: hljs }) => {
+            hljs.highlightBlock(codeEle as HTMLElement);
+          })
+          // Without this a failed chunk is an unhandled rejection; the cost 
is an unhighlighted block.
+          .catch(() => undefined);
       }
 
       const scripts = Array.from(container.querySelectorAll('script'));
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/templates/SingleResultRenderer.spec.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/templates/SingleResultRenderer.spec.tsx
new file mode 100644
index 0000000000..3240b42f11
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/templates/SingleResultRenderer.spec.tsx
@@ -0,0 +1,85 @@
+/*
+ * 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 { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import { DatasetType, ParagraphConfigResults, ParagraphIResultsMsgItem } from 
'@zeppelin/sdk';
+import { SingleResultRenderer } from './SingleResultRenderer';
+
+const result = (type: DatasetType, data: string): ParagraphIResultsMsgItem => 
({ type, data });
+
+const TABLE_DATA = 'name\tage\nalice\t30';
+
+// Index 0 stays a table, index 1 draws a chart, so reading the wrong entry 
shows.
+const configs = {
+  0: { graph: { mode: 'table' } },
+  1: { graph: { mode: 'multiBarChart' } }
+} as unknown as ParagraphConfigResults;
+
+describe('SingleResultRenderer', () => {
+  it('renders TEXT as text, leaving markup in it literal', () => {
+    // Markup in the payload is what separates this arm from HTML: routing 
TEXT to
+    // HTMLRenderer would parse the tag away instead of showing it.
+    render(<SingleResultRenderer index={0} result={result(DatasetType.TEXT, 
'line one <b>not bold</b>')} />);
+
+    expect(screen.getByText(/line one <b>not bold<\/b>/)).toBeTruthy();
+  });
+
+  it('renders TABLE through the visualization', () => {
+    render(<SingleResultRenderer index={0} result={result(DatasetType.TABLE, 
TABLE_DATA)} />);
+
+    // Only that the arm was taken. The visualization's display-mode state is a
+    // known defect (projects/zeppelin-react/AGENTS.md), so nothing here pins 
it.
+    expect(screen.getByText('alice')).toBeTruthy();
+    expect(screen.getByRole('button', { name: /Bar Chart/ })).toBeTruthy();
+  });
+
+  it('hands the visualization the display config for its own result index', () 
=> {
+    // Both indices are rendered: index 1 alone would pass against a 
hard-coded [1],
+    // and the positive assertion keeps an absent chart from reading as 
success.
+    const table = render(
+      <SingleResultRenderer index={0} config={configs} 
result={result(DatasetType.TABLE, TABLE_DATA)} />
+    );
+    expect(screen.getByText('alice')).toBeTruthy();
+    table.unmount();
+
+    render(<SingleResultRenderer index={1} config={configs} 
result={result(DatasetType.TABLE, TABLE_DATA)} />);
+    expect(screen.getByRole('button', { name: /Table/ })).toBeTruthy();
+    expect(screen.queryByText('alice')).toBeNull();
+  });
+
+  it('renders IMG as a base64 png', () => {
+    render(<SingleResultRenderer index={0} result={result(DatasetType.IMG, 
'QUJD')} />);
+
+    
expect(screen.getByRole('img').getAttribute('src')).toBe('data:image/png;base64,QUJD');
+  });
+
+  it('renders HTML as markup rather than as text', () => {
+    render(<SingleResultRenderer index={0} result={result(DatasetType.HTML, 
'<p>markup output</p>')} />);
+
+    expect(screen.getByText('markup output').tagName).toBe('P');
+  });
+
+  it('tells the user that ANGULAR results are unsupported here', () => {
+    render(<SingleResultRenderer index={0} result={result(DatasetType.ANGULAR, 
'anything')} />);
+
+    expect(screen.getByText('Angular Component')).toBeTruthy();
+    expect(screen.getByText(/not supported in React 
environment/)).toBeTruthy();
+  });
+
+  it('renders nothing for a type it has no renderer for', () => {
+    // NETWORK is declared by the SDK and reaches the default arm.
+    const { container } = render(<SingleResultRenderer index={0} 
result={result(DatasetType.NETWORK, 'graph')} />);
+
+    expect(container.innerHTML).toBe('');
+  });
+});

Reply via email to