first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,200 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import { readFileSync } from 'fs';
import type { WorkflowTestData } from 'n8n-workflow';
import path from 'path';
describe('Execute Spreadsheet File Node', () => {
const testHarness = new NodeTestHarness();
const readBinaryFile = (fileName: string) =>
readFileSync(path.resolve(__dirname, fileName), 'base64');
const loadWorkflow = (fileName: string, csvName: string) => {
const workflowData = testHarness.readWorkflowJSON(fileName);
const node = workflowData.nodes.find((n) => n.name === 'Read Binary File')!;
node.parameters.fileSelector = path.join(__dirname, csvName);
return workflowData;
};
const tests: WorkflowTestData[] = [
{
description: 'execute workflow.json',
input: {
workflowData: loadWorkflow('workflow.json', 'spreadsheet.csv'),
},
output: {
assertBinaryData: true,
nodeData: {
'Read From File': [
[
{
json: { A: 1, B: 2, C: 3 },
},
{
json: { A: 4, B: 5, C: 6 },
},
],
],
'Read From File Range': [
[
{
json: { '1': 4, '2': 5 },
},
],
],
'Read From File no Header Row': [
[
{
json: {
row: ['A', 'B', 'C'],
},
},
{
json: {
row: [1, 2, 3],
},
},
{
json: {
row: [4, 5, 6],
},
},
],
],
'Read From File Raw Data': [
[
{
json: { A: '1', B: '2', C: '3' },
},
{
json: { A: '4', B: '5', C: '6' },
},
],
],
'Read From File Read as String': [
[
{
json: { A: 1, B: 2, C: 3 },
},
{
json: { A: 4, B: 5, C: 6 },
},
],
],
'Read CSV with Row Limit': [[{ json: { A: '1', B: '2', C: '3' } }]],
'Write To File CSV': [
[
{
json: {},
binary: {
data: {
mimeType: 'text/csv',
fileType: 'text',
fileExtension: 'csv',
data: '77u/QSxCLEMKMSwyLDMKNCw1LDY=',
fileName: 'spreadsheet.csv',
fileSize: '20 B',
},
},
},
],
],
'Write To File HTML': [
[
{
json: {},
binary: {
data: {
mimeType: 'text/html',
fileType: 'html',
fileExtension: 'html',
data: readBinaryFile('spreadsheet.html'),
fileName: 'spreadsheet.html',
fileSize: '535 B',
},
},
},
],
],
// ODS file has slight differences every time it's created
//
'Write To File RTF': [
[
{
json: {},
binary: {
data: {
mimeType: 'application/rtf',
fileExtension: 'rtf',
data: readBinaryFile('spreadsheet.rtf'),
fileName: 'spreadsheet.rtf',
fileSize: '267 B',
},
},
},
],
],
'Write To File XLS': [
[
{
json: {},
binary: {
data: {
mimeType: 'application/vnd.ms-excel',
fileExtension: 'xls',
data: readBinaryFile('spreadsheet.xls'),
fileName: 'spreadsheet.xls',
fileSize: '3.58 kB',
},
},
},
],
],
},
},
},
{
description: 'execute workflow.bom.json',
input: {
workflowData: loadWorkflow('workflow.bom.json', 'bom.csv'),
},
output: {
nodeData: {
'Edit with BOM included': [[{ json: { X: null } }]],
'Edit with BOM excluded': [[{ json: { X: '1' } }]],
},
},
},
{
description: 'execute includeempty.json',
input: {
workflowData: loadWorkflow('workflow.empty.json', 'includeempty.csv'),
},
output: {
nodeData: {
'Include Empty': [[{ json: { A: '1', B: '', C: '3' } }]],
'Ignore Empty': [[{ json: { A: '1', C: '3' } }]],
},
},
},
{
description: 'execute utf8.json',
input: {
workflowData: loadWorkflow('workflow.utf8.json', 'utf8.csv'),
},
output: {
nodeData: {
'Parse UTF8 v1': [
[{ json: { A: 1, B: '株式会社', C: 3 } }, { json: { A: 4, B: 5, C: '🐛' } }],
],
'Parse UTF8 v2': [
[{ json: { A: '1', B: '株式会社', C: '3' } }, { json: { A: '4', B: '5', C: '🐛' } }],
],
},
},
},
];
for (const testData of tests) {
testHarness.setupTest(testData);
}
});
@@ -0,0 +1,2 @@
a,b,c
1,2,3
1 a b c
2 1 2 3
@@ -0,0 +1,800 @@
import { mockDeep } from 'jest-mock-extended';
import type { IBinaryData, IExecuteFunctions, INode, INodeExecutionData } from 'n8n-workflow';
import { BINARY_ENCODING, NodeOperationError } from 'n8n-workflow';
import { Readable } from 'stream';
jest.mock('xlsx', () => ({
read: jest.fn(),
utils: {
sheet_to_json: jest.fn(),
},
}));
import { read as xlsxRead, utils as xlsxUtils } from 'xlsx';
import { execute } from '../v2/fromFile.operation';
describe('fromFile.operation - xlsx parsing logic', () => {
const mockExecuteFunctions = mockDeep<IExecuteFunctions>();
const mockBinaryDataInMemory: IBinaryData = {
data: 'dGVzdCBkYXRh',
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
fileExtension: 'xlsx',
fileName: 'test.xlsx',
};
const mockBinaryDataWithId: IBinaryData = {
id: 'binary-data-id-123',
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
fileExtension: 'xlsx',
fileName: 'test.xlsx',
data: '',
};
const mockWorkbook = {
SheetNames: ['Sheet1', 'Sheet2'],
Sheets: {
Sheet1: {
A1: { t: 's', v: 'Name' },
B1: { t: 's', v: 'Age' },
C1: { t: 's', v: 'City' },
A2: { t: 's', v: 'John' },
B2: { t: 'n', v: 25 },
C2: { t: 's', v: 'NYC' },
A3: { t: 's', v: 'Jane' },
B3: { t: 'n', v: 30 },
C3: { t: 's', v: 'LA' },
},
Sheet2: {
A1: { t: 's', v: 'Product' },
B1: { t: 'n', v: 100 },
},
},
};
const mockParsedData = [
{ Name: 'John', Age: 25, City: 'NYC' },
{ Name: 'Jane', Age: 30, City: 'LA' },
];
beforeEach(() => {
jest.clearAllMocks();
mockExecuteFunctions.getNodeParameter.mockImplementation(
(paramName: string, _itemIndex: number, defaultValue?: any) => {
switch (paramName) {
case 'fileFormat':
return 'xlsx';
case 'binaryPropertyName':
return 'data';
case 'options':
return {};
default:
return defaultValue;
}
},
);
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataInMemory);
mockExecuteFunctions.getNode.mockReturnValue({
name: 'SpreadsheetFile',
type: 'n8n-nodes-base.spreadsheetFile',
id: 'test-node-id',
} as INode);
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
(xlsxRead as jest.Mock).mockReturnValue(mockWorkbook);
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue(mockParsedData);
});
describe('Basic xlsx parsing', () => {
it('should parse xlsx file from in-memory binary data', async () => {
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
expect(result).toHaveLength(2);
expect(result[0].json).toEqual({ Name: 'John', Age: 25, City: 'NYC' });
expect(result[1].json).toEqual({ Name: 'Jane', Age: 30, City: 'LA' });
expect(result[0].pairedItem).toEqual({ item: 0 });
expect(result[1].pairedItem).toEqual({ item: 0 });
expect(xlsxRead).toHaveBeenCalledWith(
Buffer.from(mockBinaryDataInMemory.data, BINARY_ENCODING),
{ raw: undefined },
);
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet1, {});
});
it('should parse xlsx file from filesystem binary data', async () => {
const mockStream = new Readable();
mockStream.push(Buffer.from('test xlsx content'));
mockStream.push(null);
const mockBuffer = Buffer.from('test xlsx content');
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataWithId);
mockExecuteFunctions.helpers.getBinaryStream.mockResolvedValue(mockStream);
mockExecuteFunctions.helpers.binaryToBuffer.mockResolvedValue(mockBuffer);
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
expect(result).toHaveLength(2);
expect(mockExecuteFunctions.helpers.getBinaryStream).toHaveBeenCalledWith(
'binary-data-id-123',
262144,
);
expect(mockExecuteFunctions.helpers.binaryToBuffer).toHaveBeenCalledWith(mockStream);
expect(xlsxRead).toHaveBeenCalledWith(mockBuffer, { raw: undefined });
});
});
describe('Options handling', () => {
it('should respect rawData option', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { rawData: true };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await execute.call(mockExecuteFunctions, items);
expect(xlsxRead).toHaveBeenCalledWith(expect.any(Buffer), { raw: true });
});
it('should respect readAsString option', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { readAsString: true };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await execute.call(mockExecuteFunctions, items);
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
});
it('should use specified sheet name', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { sheetName: 'Sheet2' };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await execute.call(mockExecuteFunctions, items);
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet2, {});
});
it('should handle range option as string', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { range: 'A1:B2' };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await execute.call(mockExecuteFunctions, items);
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet1, {
range: 'A1:B2',
});
});
it('should handle range option as number', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { range: '2' };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await execute.call(mockExecuteFunctions, items);
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet1, {
range: 2,
});
});
it('should include empty cells when option is set', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { includeEmptyCells: true };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await execute.call(mockExecuteFunctions, items);
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet1, {
defval: '',
});
});
it('should handle headerRow=false option', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { headerRow: false };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const mockArrayData = [
['Name', 'Age'],
['John', 25],
['Jane', 30],
];
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue(mockArrayData);
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet1, {
header: 1,
});
expect(result).toHaveLength(3);
expect(result[0].json).toEqual({ row: ['Name', 'Age'] });
expect(result[1].json).toEqual({ row: ['John', 25] });
expect(result[2].json).toEqual({ row: ['Jane', 30] });
});
});
describe('Error handling', () => {
it('should throw error when workbook has no sheets', async () => {
const emptyWorkbook = { SheetNames: [], Sheets: {} };
(xlsxRead as jest.Mock).mockReturnValue(emptyWorkbook);
const items: INodeExecutionData[] = [{ json: {} }];
await expect(execute.call(mockExecuteFunctions, items)).rejects.toThrow(NodeOperationError);
});
it('should throw error when specified sheet does not exist', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { sheetName: 'NonExistentSheet' };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await expect(execute.call(mockExecuteFunctions, items)).rejects.toThrow(NodeOperationError);
});
it('should handle continueOnFail gracefully', async () => {
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
(xlsxRead as jest.Mock).mockImplementation(() => {
throw new Error('Invalid file format');
});
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
expect(result).toHaveLength(1);
expect(result[0].json.error).toContain('Invalid file format');
expect(result[0].pairedItem).toEqual({ item: 0 });
});
it('should enhance error message when file extension does not match format', async () => {
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue({
...mockBinaryDataInMemory,
fileExtension: 'pdf',
});
(xlsxRead as jest.Mock).mockImplementation(() => {
throw new Error('Parse error');
});
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
expect(result[0].json.error).toContain('not in xlsx format');
});
});
describe('Multiple items processing', () => {
it('should process multiple items correctly', async () => {
const items: INodeExecutionData[] = [{ json: { id: 1 } }, { json: { id: 2 } }];
mockExecuteFunctions.helpers.assertBinaryData
.mockReturnValueOnce(mockBinaryDataInMemory)
.mockReturnValueOnce({
...mockBinaryDataInMemory,
fileName: 'test2.xlsx',
});
const result = await execute.call(mockExecuteFunctions, items);
expect(result).toHaveLength(4);
expect(result[0].pairedItem).toEqual({ item: 0 });
expect(result[1].pairedItem).toEqual({ item: 0 });
expect(result[2].pairedItem).toEqual({ item: 1 });
expect(result[3].pairedItem).toEqual({ item: 1 });
});
});
describe('File format detection', () => {
it('should handle autodetect for xlsx files', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'fileFormat') return 'autodetect';
if (paramName === 'binaryPropertyName') return 'data';
if (paramName === 'options') return {};
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
expect(result).toHaveLength(2);
expect(xlsxRead).toHaveBeenCalled();
});
});
describe('Additional edge cases', () => {
it('should handle mixed file formats with autodetect', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'fileFormat') return 'autodetect';
if (paramName === 'binaryPropertyName') return 'data';
if (paramName === 'options') return {};
return undefined;
});
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue({
...mockBinaryDataInMemory,
mimeType: 'application/octet-stream',
fileExtension: 'xlsx',
});
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
expect(result).toHaveLength(2);
expect(xlsxRead).toHaveBeenCalled();
});
it('should handle custom binary property name correctly', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'binaryPropertyName') return 'customBinaryField';
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'options') return {};
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await execute.call(mockExecuteFunctions, items);
expect(mockExecuteFunctions.helpers.assertBinaryData).toHaveBeenCalledWith(
0,
'customBinaryField',
);
});
it('should handle binary data stream errors gracefully', async () => {
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataWithId);
mockExecuteFunctions.helpers.getBinaryStream.mockRejectedValue(new Error('Stream error'));
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
expect(result).toHaveLength(1);
expect(result[0].json.error).toContain('Stream error');
});
});
describe('Binary string conversion', () => {
it('should convert buffer to binary string when readAsString is true', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { readAsString: true };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await execute.call(mockExecuteFunctions, items);
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
const callArgs = (xlsxRead as jest.Mock).mock.calls[0];
const passedData = callArgs[0];
const expectedBinaryString = Buffer.from(
mockBinaryDataInMemory.data,
BINARY_ENCODING,
).toString('binary');
expect(passedData).toBe(expectedBinaryString);
});
it('should use buffer directly when readAsString is false', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { readAsString: false };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await execute.call(mockExecuteFunctions, items);
// Verify that xlsxRead was called with a Buffer (no type specified)
expect(xlsxRead).toHaveBeenCalledWith(expect.any(Buffer), { raw: undefined });
});
it('should handle readAsString with filesystem binary data', async () => {
const mockStream = new Readable();
mockStream.push(Buffer.from('test xlsx content'));
mockStream.push(null);
const mockBuffer = Buffer.from('test xlsx content');
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { readAsString: true };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataWithId);
mockExecuteFunctions.helpers.getBinaryStream.mockResolvedValue(mockStream);
mockExecuteFunctions.helpers.binaryToBuffer.mockResolvedValue(mockBuffer);
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
expect(result).toHaveLength(2);
expect(mockExecuteFunctions.helpers.getBinaryStream).toHaveBeenCalledWith(
'binary-data-id-123',
262144,
);
expect(mockExecuteFunctions.helpers.binaryToBuffer).toHaveBeenCalledWith(mockStream);
// Verify that xlsxRead was called with binary string
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
// Verify the string is the result of buffer.toString('binary')
const callArgs = (xlsxRead as jest.Mock).mock.calls[0];
const passedData = callArgs[0];
const expectedBinaryString = mockBuffer.toString('binary');
expect(passedData).toBe(expectedBinaryString);
});
it('should combine readAsString with other options correctly', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options')
return { readAsString: true, rawData: true, sheetName: 'Sheet2' };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await execute.call(mockExecuteFunctions, items);
// Verify that xlsxRead was called with binary string and rawData option
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: true, type: 'binary' });
// Verify that the correct sheet was used
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet2, {});
});
});
describe('Special character handling', () => {
it('should handle special characters correctly when readAsString is true', async () => {
// Mock binary data that contains special characters (e.g., accented characters, emojis)
const specialCharBinaryData: IBinaryData = {
data: Buffer.from('Special chars: àáâãäåæçèéêë 🚀 ñöü', 'utf8').toString(BINARY_ENCODING),
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
fileExtension: 'xlsx',
fileName: 'special-chars.xlsx',
};
const mockWorkbookWithSpecialChars = {
SheetNames: ['Sheet1'],
Sheets: {
Sheet1: {
A1: { t: 's', v: 'Special chars: àáâãäåæçèéêë 🚀 ñöü' },
A2: { t: 's', v: 'Café' },
A3: { t: 's', v: 'Naïve résumé' },
},
},
};
const mockSpecialCharData = [
{ text: 'Special chars: àáâãäåæçèéêë 🚀 ñöü' },
{ text: 'Café' },
{ text: 'Naïve résumé' },
];
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { readAsString: true };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(specialCharBinaryData);
(xlsxRead as jest.Mock).mockReturnValue(mockWorkbookWithSpecialChars);
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue(mockSpecialCharData);
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
// Verify that xlsxRead was called with binary string type for proper character handling
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
// Verify that special characters are preserved in the output
expect(result).toHaveLength(3);
expect(result[0].json.text).toBe('Special chars: àáâãäåæçèéêë 🚀 ñöü');
expect(result[1].json.text).toBe('Café');
expect(result[2].json.text).toBe('Naïve résumé');
});
it('should demonstrate the difference between readAsString true vs false for character encoding', async () => {
// Test data with potential encoding issues
const encodingTestData: IBinaryData = {
data: Buffer.from('Encoding test: café naïve résumé', 'utf8').toString(BINARY_ENCODING),
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
fileExtension: 'xlsx',
fileName: 'encoding-test.xlsx',
};
const mockWorkbookEncoding = {
SheetNames: ['Sheet1'],
Sheets: {
Sheet1: {
A1: { t: 's', v: 'Encoding test: café naïve résumé' },
},
},
};
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(encodingTestData);
(xlsxRead as jest.Mock).mockReturnValue(mockWorkbookEncoding);
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue([
{ text: 'Encoding test: café naïve résumé' },
]);
// Test with readAsString: true
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { readAsString: true };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
await execute.call(mockExecuteFunctions, items);
// Verify that when readAsString is true, we use binary type for proper character handling
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
// Reset mocks for second test
jest.clearAllMocks();
(xlsxRead as jest.Mock).mockReturnValue(mockWorkbookEncoding);
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue([
{ text: 'Encoding test: café naïve résumé' },
]);
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(encodingTestData);
// Test with readAsString: false (default)
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { readAsString: false };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
await execute.call(mockExecuteFunctions, items);
// Verify that when readAsString is false, we use buffer directly (no type specified)
expect(xlsxRead).toHaveBeenCalledWith(expect.any(Buffer), { raw: undefined });
});
it('should handle various international characters when readAsString is enabled', async () => {
// Test with various international characters that might cause encoding issues
const internationalChars = [
'Chinese: 你好世界',
'Japanese: こんにちは',
'Korean: 안녕하세요',
'Arabic: مرحبا',
'Russian: Привет',
'Greek: Γεια σας',
'Hebrew: שלום',
'Thai: สวัสดี',
];
const internationalBinaryData: IBinaryData = {
data: Buffer.from(internationalChars.join('\n'), 'utf8').toString(BINARY_ENCODING),
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
fileExtension: 'xlsx',
fileName: 'international.xlsx',
};
const mockInternationalWorkbook = {
SheetNames: ['Sheet1'],
Sheets: {
Sheet1: internationalChars.reduce((acc, char, index) => {
acc[`A${index + 1}`] = { t: 's', v: char };
return acc;
}, {} as any),
},
};
const mockInternationalData = internationalChars.map((char) => ({ text: char }));
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'options') return { readAsString: true };
if (paramName === 'fileFormat') return 'xlsx';
if (paramName === 'binaryPropertyName') return 'data';
return undefined;
});
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(internationalBinaryData);
(xlsxRead as jest.Mock).mockReturnValue(mockInternationalWorkbook);
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue(mockInternationalData);
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
// Verify that xlsxRead was called with binary string type
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
// Verify that all international characters are preserved
expect(result).toHaveLength(8);
internationalChars.forEach((expectedChar, index) => {
expect(result[index].json.text).toBe(expectedChar);
});
});
});
describe('CSV parsing with skipRecordsWithErrors', () => {
const invalidCsvData = 'id,name\n3,"John"\n1,"Alice\n2,"Bob"';
const mockBinaryDataCSV: IBinaryData = {
data: Buffer.from(invalidCsvData, 'utf8').toString(BINARY_ENCODING),
mimeType: 'text/csv',
fileExtension: 'csv',
fileName: 'test.csv',
};
beforeEach(() => {
jest.clearAllMocks();
mockExecuteFunctions.getNodeParameter.mockImplementation(
(paramName: string, _itemIndex: number, defaultValue?: any) => {
switch (paramName) {
case 'fileFormat':
return 'csv';
case 'binaryPropertyName':
return 'data';
case 'options':
return {};
default:
return defaultValue;
}
},
);
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataCSV);
mockExecuteFunctions.getNode.mockReturnValue({
name: 'SpreadsheetFile',
type: 'n8n-nodes-base.spreadsheetFile',
id: 'test-node-id',
} as INode);
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
});
it('should skip records with errors when skipRecordsWithErrors is enabled with limit -1', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'fileFormat') return 'csv';
if (paramName === 'binaryPropertyName') return 'data';
if (paramName === 'options')
return {
skipRecordsWithErrors: { value: { enabled: true, maxSkippedRecords: -1 } },
columns: true,
};
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
// Should have 1 valid record (John), Bob and Alice is considered a single record with error
expect(result).toHaveLength(1);
expect(result[0].json).toEqual({ id: '3', name: 'John' });
});
it('should skip records with errors when skipRecordsWithErrors is enabled with limit 1', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'fileFormat') return 'csv';
if (paramName === 'binaryPropertyName') return 'data';
if (paramName === 'options')
return {
skipRecordsWithErrors: { value: { enabled: true, maxSkippedRecords: 1 } },
columns: true,
};
return undefined;
});
const items: INodeExecutionData[] = [{ json: {} }];
const result = await execute.call(mockExecuteFunctions, items);
expect(result).toHaveLength(1);
expect(result[0].json).toEqual({ id: '3', name: 'John' });
expect(result[0].pairedItem).toEqual({ item: 0 });
});
it('should throw error when skipped records exceed maxSkippedRecords limit', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'fileFormat') return 'csv';
if (paramName === 'binaryPropertyName') return 'data';
if (paramName === 'options')
return {
skipRecordsWithErrors: { value: { enabled: true, maxSkippedRecords: 1 } },
columns: true,
};
return undefined;
});
const csvWithThreeErrors =
'id,name\n3,"John"\n1,"Alice\n2,"Bob"\n4,"Charlie\n5,"Eve\n6,"David';
const mockBinaryDataThreeErrors: IBinaryData = {
data: Buffer.from(csvWithThreeErrors, 'utf8').toString(BINARY_ENCODING),
mimeType: 'text/csv',
fileExtension: 'csv',
fileName: 'test-three-errors.csv',
};
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataThreeErrors);
const items: INodeExecutionData[] = [{ json: {} }];
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'fileFormat') return 'csv';
if (paramName === 'binaryPropertyName') return 'data';
if (paramName === 'options')
return {
skipRecordsWithErrors: { value: { enabled: true, maxSkippedRecords: 1 } },
columns: true,
};
return undefined;
});
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataThreeErrors);
await expect(execute.call(mockExecuteFunctions, items)).rejects.toThrow(
'Max number of skipped records exceeded',
);
});
});
});
@@ -0,0 +1,2 @@
A,B,C
1,,3
1 A B C
2 1 3
@@ -0,0 +1,3 @@
A,B,C
1,2,3
4,5,6
1 A B C
2 1 2 3
3 4 5 6
@@ -0,0 +1 @@
<html><head><meta charset="utf-8"/><title>SheetJS Table Export</title></head><body><table><tr><td data-t="s" data-v="A" id="sjs-A1">A</td><td data-t="s" data-v="B" id="sjs-B1">B</td><td data-t="s" data-v="C" id="sjs-C1">C</td></tr><tr><td data-t="n" data-v="1" id="sjs-A2">1</td><td data-t="n" data-v="2" id="sjs-B2">2</td><td data-t="n" data-v="3" id="sjs-C2">3</td></tr><tr><td data-t="n" data-v="4" id="sjs-A3">4</td><td data-t="n" data-v="5" id="sjs-B3">5</td><td data-t="n" data-v="6" id="sjs-C3">6</td></tr></table></body></html>
@@ -0,0 +1 @@
{\rtf1\ansi\trowd\trautofit1\cellx1\cellx2\cellx3\pard\intbl A\cell B\cell C\cell\pard\intbl\row\trowd\trautofit1\cellx1\cellx2\cellx3\pard\intbl 1\cell 2\cell 3\cell\pard\intbl\row\trowd\trautofit1\cellx1\cellx2\cellx3\pard\intbl 4\cell 5\cell 6\cell\pard\intbl\row}
@@ -0,0 +1,3 @@
A,B,C
1,株式会社,3
4,5,🐛
1 A B C
2 1 株式会社 3
3 4 5 🐛
@@ -0,0 +1,155 @@
{
"nodes": [
{
"parameters": {},
"id": "40bf604f-19f9-43e7-8bbb-74c36925f154",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-320,
1040
]
},
{
"parameters": {
"fileSelector": "bom.csv"
},
"id": "623ea890-8882-4273-973e-834652d823b5",
"name": "Read Binary File",
"type": "n8n-nodes-base.readBinaryFiles",
"typeVersion": 1,
"position": [
-100,
1040
]
},
{
"parameters": {
"fileFormat": "csv",
"options": {
"enableBOM": true
}
},
"id": "c8cca5fb-e119-4ca1-a597-4f051a7f64ea",
"name": "Exclude BOM",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 2,
"position": [
120,
960
]
},
{
"parameters": {
"fileFormat": "csv",
"options": {
"enableBOM": false
}
},
"id": "56ec11dc-966b-4d06-b8c0-61475b30333d",
"name": "Include BOM",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 2,
"position": [
120,
1180
]
},
{
"parameters": {
"fields": {
"values": [
{
"name": "X",
"stringValue": "={{ $json.a }}"
}
]
},
"include": "none",
"options": {}
},
"id": "6f6bccf2-d674-4774-9df9-6f6fd893bace",
"name": "Edit with BOM excluded",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
320,
960
]
},
{
"parameters": {
"fields": {
"values": [
{
"name": "X",
"stringValue": "={{ $json.a }}"
}
]
},
"include": "none",
"options": {}
},
"id": "27ca5cde-19cb-4bf2-9ab4-7f7e77ad01bd",
"name": "Edit with BOM included",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
320,
1180
]
}
],
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Read Binary File",
"type": "main",
"index": 0
}
]
]
},
"Exclude BOM": {
"main": [
[
{
"node": "Edit with BOM excluded",
"type": "main",
"index": 0
}
]
]
},
"Include BOM": {
"main": [
[
{
"node": "Edit with BOM included",
"type": "main",
"index": 0
}
]
]
},
"Read Binary File": {
"main": [
[
{
"node": "Exclude BOM",
"type": "main",
"index": 0
},
{
"node": "Include BOM",
"type": "main",
"index": 0
}
]
]
}
}
}
@@ -0,0 +1,90 @@
{
"meta": {},
"nodes": [
{
"parameters": {
"fileFormat": "csv",
"options": {
"includeEmptyCells": false
}
},
"id": "8aed098d-3c0b-43c9-b7e8-c4106c88b409",
"name": "Ignore Empty",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 2,
"position": [
1160,
500
]
},
{
"parameters": {},
"id": "649db2c5-27dc-4cec-b084-8982632311e7",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
720,
360
]
},
{
"parameters": {
"fileSelector": "includeempty.csv"
},
"id": "5056b8c4-fb6e-4ca1-9bc1-33f5db4027ad",
"name": "Read Binary File",
"type": "n8n-nodes-base.readBinaryFiles",
"typeVersion": 1,
"position": [
940,
360
]
},
{
"parameters": {
"fileFormat": "csv",
"options": {
"includeEmptyCells": true
}
},
"id": "a4822e75-d638-45c8-887f-0487d5237267",
"name": "Include Empty",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 2,
"position": [
1160,
280
]
}
],
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Read Binary File",
"type": "main",
"index": 0
}
]
]
},
"Read Binary File": {
"main": [
[
{
"node": "Include Empty",
"type": "main",
"index": 0
},
{
"node": "Ignore Empty",
"type": "main",
"index": 0
}
]
]
}
}
}
@@ -0,0 +1,271 @@
{
"nodes": [
{
"parameters": {},
"id": "087277cc-297d-4912-bd11-86626eff2d71",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
620,
640
]
},
{
"parameters": {
"options": {}
},
"id": "f55bc21c-c9a8-43af-bbc8-e4bdd30f0ce9",
"name": "Read From File",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 1,
"position": [
1260,
640
]
},
{
"parameters": {
"fileSelector": "spreadsheet.csv"
},
"id": "d7620053-eb3d-43dd-b2cd-d60d9a08a9cc",
"name": "Read Binary File",
"type": "n8n-nodes-base.readBinaryFiles",
"typeVersion": 1,
"position": [
840,
640
]
},
{
"parameters": {
"operation": "toFile",
"fileFormat": "csv",
"options": {}
},
"id": "21bc49fe-1e6b-46d6-a04d-cb474d138e02",
"name": "Write To File CSV",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 1,
"position": [
1580,
280
]
},
{
"parameters": {
"operation": "toFile",
"fileFormat": "html",
"options": {}
},
"id": "a4c2c717-5a9d-4fd6-8450-6bb78e233c05",
"name": "Write To File HTML",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 1,
"position": [
1580,
460
]
},
{
"parameters": {
"operation": "toFile",
"fileFormat": "ods",
"options": {}
},
"id": "58e5a423-0477-44df-a505-6b8a40dbf275",
"name": "Write To File ODS",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 1,
"position": [
1580,
640
]
},
{
"parameters": {
"operation": "toFile",
"fileFormat": "rtf",
"options": {}
},
"id": "3ae6e9c5-bc0a-44e4-959e-cfc572f4179f",
"name": "Write To File RTF",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 1,
"position": [
1580,
820
]
},
{
"parameters": {
"operation": "toFile",
"options": {}
},
"id": "7e6db847-d24c-4094-907d-92ffec626f68",
"name": "Write To File XLS",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 1,
"position": [
1580,
1020
]
},
{
"parameters": {
"options": {
"range": "A2:B3"
}
},
"id": "48934f0d-ac10-4862-ae0c-2ea591b111e3",
"name": "Read From File Range",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 1,
"position": [
1060,
520
]
},
{
"parameters": {
"options": {
"headerRow": false
}
},
"id": "dea6f3f6-f2fb-472e-97b2-8a0c0a36bc4d",
"name": "Read From File no Header Row",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 1,
"position": [
1060,
320
]
},
{
"parameters": {
"options": {
"rawData": true
}
},
"id": "38ed33fc-6906-4b09-8376-937ef3ca99be",
"name": "Read From File Raw Data",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 1,
"position": [
1060,
740
]
},
{
"parameters": {
"options": {
"readAsString": true
}
},
"id": "ffe09dc8-9b7a-4baf-bd03-bd65bbce2590",
"name": "Read From File Read as String",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 1,
"position": [
1060,
940
]
},
{
"parameters": {
"fileFormat": "csv",
"options": {
"maxRowCount": 1
}
},
"id": "de905389-a11b-4dd8-8416-14d650804445",
"name": "Read CSV with Row Limit",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 2,
"position": [
-60,
1340
]
}
],
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Read Binary File",
"type": "main",
"index": 0
}
]
]
},
"Read From File": {
"main": [
[
{
"node": "Write To File CSV",
"type": "main",
"index": 0
},
{
"node": "Write To File HTML",
"type": "main",
"index": 0
},
{
"node": "Write To File ODS",
"type": "main",
"index": 0
},
{
"node": "Write To File RTF",
"type": "main",
"index": 0
},
{
"node": "Write To File XLS",
"type": "main",
"index": 0
}
]
]
},
"Read Binary File": {
"main": [
[
{
"node": "Read From File",
"type": "main",
"index": 0
},
{
"node": "Read From File Range",
"type": "main",
"index": 0
},
{
"node": "Read From File no Header Row",
"type": "main",
"index": 0
},
{
"node": "Read From File Raw Data",
"type": "main",
"index": 0
},
{
"node": "Read From File Read as String",
"type": "main",
"index": 0
},
{
"node": "Read CSV with Row Limit",
"type": "main",
"index": 0
}
]
]
}
}
}
@@ -0,0 +1,91 @@
{
"meta": {
"instanceId": "78577815012af39cf16dad7a787b0898c42fb7514b8a7f99b2136862c2af502c"
},
"nodes": [
{
"parameters": {},
"id": "2130ab19-2efb-4217-b234-f8607d4122cc",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
260,
460
]
},
{
"parameters": {
"options": {
"readAsString": true
}
},
"id": "68e03042-aa27-43db-bfec-3c4fe07ce9f6",
"name": "Parse UTF8 v1",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 1,
"position": [
760,
360
]
},
{
"parameters": {
"fileFormat": "csv",
"options": {
"readAsString": true
}
},
"id": "6a8b7ee9-5d14-4b67-b7cc-afee6bcc1fa6",
"name": "Parse UTF8 v2",
"type": "n8n-nodes-base.spreadsheetFile",
"typeVersion": 2,
"position": [
760,
560
]
},
{
"parameters": {
"fileSelector": "utf8.csv"
},
"id": "623ea890-8882-4273-973e-834652d823b5",
"name": "Read Binary File",
"type": "n8n-nodes-base.readBinaryFiles",
"typeVersion": 1,
"position": [
480,
460
]
}
],
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Read Binary File",
"type": "main",
"index": 0
}
]
]
},
"Read Binary File": {
"main": [
[
{
"node": "Parse UTF8 v1",
"type": "main",
"index": 0
},
{
"node": "Parse UTF8 v2",
"type": "main",
"index": 0
}
]
]
}
}
}