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

tbonelee 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 1a7f475d33 [ZEPPELIN-6649] Add unit tests for the remaining shared 
utilities (shell + remote)
1a7f475d33 is described below

commit 1a7f475d33884b675c81e793380e1fef180338c1
Author: huiseong29 <[email protected]>
AuthorDate: Mon Oct 5 14:04:23 2026 +0900

    [ZEPPELIN-6649] Add unit tests for the remaining shared utilities (shell + 
remote)
    
    ### What is this PR for?
    Adds unit tests for the three utilities left over in ZEPPELIN-6649, 
following the conventions in `zeppelin-web-angular/AGENTS.md`.
    
    - `isRecord` (`src/app/utility/type-utility.ts`): the `null` and array 
boundaries, primitives, functions, and an object without a prototype.
    - `parseTableData` (`projects/zeppelin-react/src/utils/tableUtils.ts`): 
header/rows split on tabs, header-only input, trailing newline, and an empty 
cell between tabs.
    - `exportFile` (`projects/zeppelin-react/src/utils/exportFile.ts`): extends 
the existing spec with multi-row CSV assembly, the xlsx branch (file name, MIME 
type, and header plus rows read back from `Sheet1`), and the empty-data guard 
for xlsx and for data without `rows`.
    
    Spec files only; no production code is changed. The files the issue lists 
as out of scope (`element.ts`, `css-unit-conversion.ts`, `line-map.ts`) are not 
touched.
    
    Not pinned on purpose, since the baseline says not to record behaviour that 
is already questionable: `parseTableData` trims the whole input, so a leading 
empty header cell loses its tab (`"\tb\n1\t2"` gives `["b"]`); CRLF input 
leaves a trailing `\r` on each cell at the end of a line; an empty string 
yields one empty column name. I can open a follow-up issue for these if useful.
    
    ### What type of PR is it?
    Improvement
    
    ### Todos
    * [x] - Add unit tests for `isRecord`, `parseTableData` and `exportFile`
    
    ### What is the Jira issue?
    * https://issues.apache.org/jira/browse/ZEPPELIN-6649
    
    ### How should this be tested?
    * `cd zeppelin-web-angular && npm run test:shell -- type-utility.spec.ts` 
(10 tests)
    * `cd zeppelin-web-angular/projects/zeppelin-react && npm test && npm run 
lint` (18 tests in `src/utils`)
    * The React suite does not run in pull-request CI yet (see ZEPPELIN-6566), 
so it was run locally.
    
    ### Screenshots (if appropriate)
    
    ### Questions:
    * Does the license files need to update? No.
    * Is there breaking changes for older versions? No.
    * Does this needs documentation? No.
    
    Closes #5524 from huiseong29/ZEPPELIN-6649.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 .../zeppelin-react/src/utils/exportFile.spec.ts    | 62 ++++++++++++++++++++++
 .../zeppelin-react/src/utils/tableUtils.spec.ts    | 39 ++++++++++++++
 .../src/app/utility/type-utility.spec.ts           | 46 ++++++++++++++++
 3 files changed, 147 insertions(+)

diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/utils/exportFile.spec.ts 
b/zeppelin-web-angular/projects/zeppelin-react/src/utils/exportFile.spec.ts
index adc654cbff..d4ead52faa 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/src/utils/exportFile.spec.ts
+++ b/zeppelin-web-angular/projects/zeppelin-react/src/utils/exportFile.spec.ts
@@ -10,9 +10,11 @@
  * limitations under the License.
  */
 
+import * as XLSX from 'xlsx-js-style';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 
 import { exportFile } from './exportFile';
+import type { TableData } from './tableUtils';
 
 const saveAs = vi.fn();
 vi.mock('file-saver', () => ({ saveAs: (...args: unknown[]) => saveAs(...args) 
}));
@@ -40,9 +42,69 @@ describe('exportFile', () => {
     expect(saveAs.mock.calls[0][1]).toBe('export.csv');
   });
 
+  it('joins the header and several rows with commas and newlines in the CSV', 
async () => {
+    await exportFile(
+      {
+        columnNames: ['id', 'name'],
+        rows: [
+          ['1', 'alice'],
+          ['2', 'bob']
+        ]
+      },
+      'csv'
+    );
+
+    const bytes = await savedBytes();
+    expect(new 
TextDecoder().decode(bytes.subarray(3))).toBe('id,name\n1,alice\n2,bob');
+  });
+
   it('does not export an empty table', async () => {
     await exportFile({ columnNames: ['name'], rows: [] }, 'csv');
 
     expect(saveAs).not.toHaveBeenCalled();
   });
+
+  it('does not export an empty table as xlsx either', async () => {
+    await exportFile({ columnNames: ['name'], rows: [] }, 'xlsx');
+
+    expect(saveAs).not.toHaveBeenCalled();
+  });
+
+  it('does not export when the table data has no rows at all', async () => {
+    await exportFile({ columnNames: ['name'] } as unknown as TableData, 'csv');
+    await exportFile(undefined as unknown as TableData, 'csv');
+
+    expect(saveAs).not.toHaveBeenCalled();
+  });
+
+  it('saves xlsx as export.xlsx with the spreadsheet MIME type', async () => {
+    await exportFile({ columnNames: ['name', 'city'], rows: [['alice', '서울']] 
}, 'xlsx');
+
+    expect(saveAs).toHaveBeenCalledOnce();
+    const blob = saveAs.mock.calls[0][0] as Blob;
+    expect(blob.type).toContain('spreadsheetml.sheet');
+    expect(saveAs.mock.calls[0][1]).toBe('export.xlsx');
+  });
+
+  it('writes the header and the rows to Sheet1 of the xlsx file', async () => {
+    await exportFile(
+      {
+        columnNames: ['name', 'city'],
+        rows: [
+          ['alice', '서울'],
+          ['bob', 'Busan']
+        ]
+      },
+      'xlsx'
+    );
+
+    const bytes = await savedBytes();
+    const workbook = XLSX.read(bytes, { type: 'array' });
+    expect(workbook.SheetNames).toEqual(['Sheet1']);
+    expect(XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1'], { header: 1 
})).toEqual([
+      ['name', 'city'],
+      ['alice', '서울'],
+      ['bob', 'Busan']
+    ]);
+  });
 });
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/utils/tableUtils.spec.ts 
b/zeppelin-web-angular/projects/zeppelin-react/src/utils/tableUtils.spec.ts
new file mode 100644
index 0000000000..d580357ace
--- /dev/null
+++ b/zeppelin-web-angular/projects/zeppelin-react/src/utils/tableUtils.spec.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { describe, expect, it } from 'vitest';
+
+import { parseTableData } from './tableUtils';
+
+describe('parseTableData', () => {
+  it('uses the first line as column names and splits the rest into rows on 
tabs', () => {
+    expect(parseTableData('name\tcity\nalice\tSeoul\nbob\tBusan')).toEqual({
+      columnNames: ['name', 'city'],
+      rows: [
+        ['alice', 'Seoul'],
+        ['bob', 'Busan']
+      ]
+    });
+  });
+
+  it('returns no rows when the input is only a header line', () => {
+    expect(parseTableData('name\tcity')).toEqual({ columnNames: ['name', 
'city'], rows: [] });
+  });
+
+  it('ignores a trailing newline instead of producing an empty last row', () 
=> {
+    
expect(parseTableData('name\tcity\nalice\tSeoul\n').rows).toEqual([['alice', 
'Seoul']]);
+  });
+
+  it('keeps an empty cell between two tabs', () => {
+    expect(parseTableData('a\tb\tc\n1\t\t3').rows).toEqual([['1', '', '3']]);
+  });
+});
diff --git a/zeppelin-web-angular/src/app/utility/type-utility.spec.ts 
b/zeppelin-web-angular/src/app/utility/type-utility.spec.ts
new file mode 100644
index 0000000000..d3981475ef
--- /dev/null
+++ b/zeppelin-web-angular/src/app/utility/type-utility.spec.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { describe, expect, it } from 'vitest';
+
+import { isRecord } from './type-utility';
+
+describe('isRecord', () => {
+  it('accepts a plain object, empty or not', () => {
+    expect(isRecord({})).toBe(true);
+    expect(isRecord({ a: 1 })).toBe(true);
+  });
+
+  it('accepts an object without a prototype', () => {
+    expect(isRecord(Object.create(null))).toBe(true);
+  });
+
+  it('rejects null even though typeof null is "object"', () => {
+    expect(isRecord(null)).toBe(false);
+  });
+
+  it('rejects arrays, empty or not', () => {
+    expect(isRecord([])).toBe(false);
+    expect(isRecord([1, 2])).toBe(false);
+  });
+
+  it.each([
+    ['undefined', undefined],
+    ['a string', 'text'],
+    ['a number', 0],
+    ['a boolean', false],
+    ['a symbol', Symbol('s')],
+    ['a function', () => ({})]
+  ])('rejects %s', (_label, value) => {
+    expect(isRecord(value)).toBe(false);
+  });
+});

Reply via email to