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