From 98846244bc45ba6adbd73f74f76c4c8e66c9ee84 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sun, 11 May 2025 17:54:48 -0400 Subject: [PATCH 01/16] Expand Electron mock. --- src/tests/__mocks__/electron.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/tests/__mocks__/electron.js b/src/tests/__mocks__/electron.js index 3b0201e..c696ecb 100644 --- a/src/tests/__mocks__/electron.js +++ b/src/tests/__mocks__/electron.js @@ -1,3 +1,20 @@ +const mockWindow = { + webContents: { + send: jest.fn(), + }, +}; + +const mockDialog = { + showOpenDialog: jest.fn().mockResolvedValue({ + filePaths: ['/path/to/file'], + canceled: false, + }), + showSaveDialog: jest.fn().mockResolvedValue({ + filePath: '/path/to/save/file', + canceled: false, + }), +}; + module.exports = { require: jest.fn(), match: jest.fn(), @@ -6,5 +23,11 @@ module.exports = { getPath: jest.fn().mockReturnValue('/path'), getName: jest.fn().mockReturnValue('appName'), }, - dialog: jest.fn(), + BrowserWindow: jest.fn().mockReturnValue(mockWindow), + dialog: mockDialog, + ipcMain: { + on: jest.fn(), + send: jest.fn(), + }, + mainWindow: mockWindow, }; From aaebe91d4b5cee6c41d3338c33cda98fbfbb2028 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sun, 11 May 2025 17:55:24 -0400 Subject: [PATCH 02/16] Update mock handling, add window handler tests --- src/tests/sf_calls.test.js | 52 +++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/tests/sf_calls.test.js b/src/tests/sf_calls.test.js index 64298ed..8c7e5e9 100644 --- a/src/tests/sf_calls.test.js +++ b/src/tests/sf_calls.test.js @@ -1,11 +1,6 @@ // FS is used to load samples from file. const fs = require('fs'); - -// Mock knex module -jest.mock('knex'); - -// Mock jsforce module -jest.mock('jsforce'); +const electron = require('electron'); // The actual module we're testing. const sfcalls = require('../sf_calls'); @@ -383,3 +378,48 @@ test('Test recommendObjects with Education Cloud', () => { // Should not have duplicates even though Account appears in multiple feature sets expect(recommended.filter((obj) => obj === 'Account')).toHaveLength(1); }); + +test('Test setWindow function', () => { + const setwindow = sfcalls.__get__('setwindow'); + + setwindow(electron.mainWindow); + const resultWindow = sfcalls.__get__('mainWindow'); + + expect(resultWindow).toBe(electron.mainWindow); + expect(resultWindow.webContents.send).toBeDefined(); + expect(resultWindow).toHaveProperty('webContents.send'); +}); + +test('Test setPreferences function', () => { + const setPreferences = sfcalls.__get__('setPreferences'); + const mockPreferences = { + defaults: { + suppressReadOnly: true, + suppressAudit: false, + textEmptyString: true, + checkboxDefault: false, + attemptSFValues: true, + }, + indexes: { + lookups: true, + picklists: false, + externalIds: true, + }, + picklists: { + type: 'enum', + unrestricted: false, + ensureBlanks: true, + }, + lookups: { + type: 'char(18)', + }, + }; + + setPreferences(mockPreferences); + const preferences = sfcalls.__get__('preferences'); + + expect(preferences).toEqual(mockPreferences); + expect(preferences.defaults.suppressReadOnly).toBe(true); + expect(preferences.indexes.lookups).toBe(true); + expect(preferences.picklists.type).toBe('enum'); +}); From 72b9321995514b47a8af8eea1114451bbf8412a3 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sun, 11 May 2025 17:57:07 -0400 Subject: [PATCH 03/16] Add test of logMessage --- src/tests/sf_calls.test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/tests/sf_calls.test.js b/src/tests/sf_calls.test.js index 8c7e5e9..c5e36ea 100644 --- a/src/tests/sf_calls.test.js +++ b/src/tests/sf_calls.test.js @@ -423,3 +423,30 @@ test('Test setPreferences function', () => { expect(preferences.indexes.lookups).toBe(true); expect(preferences.picklists.type).toBe('enum'); }); + +test('Test logMessage function', () => { + const logMessage = sfcalls.__get__('logMessage'); + + // Set the window first + const setwindow = sfcalls.__get__('setwindow'); + setwindow(electron.mainWindow); + + // Test sending a log message + const result = logMessage('Test Title', 'Info', 'Test Message'); + + // Verify the window's send method was called with correct parameters + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'log_message', + { + sender: 'Test Title', + channel: 'Info', + message: 'Test Message', + }, + ); + + // Verify function returns true + expect(result).toBe(true); + + // Verify the send method was called exactly once + expect(electron.mainWindow.webContents.send).toHaveBeenCalledTimes(1); +}); From c7a300e5488579f192bd4d1b63af38ecd0c3423c Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sun, 11 May 2025 18:08:27 -0400 Subject: [PATCH 04/16] Add test for updateLoader --- src/tests/sf_calls.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/tests/sf_calls.test.js b/src/tests/sf_calls.test.js index c5e36ea..b48d7e6 100644 --- a/src/tests/sf_calls.test.js +++ b/src/tests/sf_calls.test.js @@ -450,3 +450,22 @@ test('Test logMessage function', () => { // Verify the send method was called exactly once expect(electron.mainWindow.webContents.send).toHaveBeenCalledTimes(1); }); + +test('Test updateLoader function', () => { + const updateLoader = sfcalls.__get__('updateLoader'); + + // Set the window first + const setwindow = sfcalls.__get__('setwindow'); + setwindow(electron.mainWindow); + + // Test updating the loader + updateLoader('Test Loading Message'); + + // Verify the window's send method was called with correct parameters + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'update_loader', + { + message: 'Test Loading Message', + }, + ); +}); From 95abb3be615d172e59ccd9587abfbab6c6eb55f6 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Mon, 26 May 2025 15:54:39 -0400 Subject: [PATCH 05/16] Adding coverage of loadSchemaFromFile --- src/tests/__mocks__/electron.js | 4 +-- src/tests/sf_calls.test.js | 52 ++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/tests/__mocks__/electron.js b/src/tests/__mocks__/electron.js index c696ecb..ec52684 100644 --- a/src/tests/__mocks__/electron.js +++ b/src/tests/__mocks__/electron.js @@ -6,11 +6,11 @@ const mockWindow = { const mockDialog = { showOpenDialog: jest.fn().mockResolvedValue({ - filePaths: ['/path/to/file'], + filePaths: ['src/tests/sampleSObjectDescribes.json'], canceled: false, }), showSaveDialog: jest.fn().mockResolvedValue({ - filePath: '/path/to/save/file', + filePath: 'path/to/save/file', canceled: false, }), }; diff --git a/src/tests/sf_calls.test.js b/src/tests/sf_calls.test.js index b48d7e6..c45b64c 100644 --- a/src/tests/sf_calls.test.js +++ b/src/tests/sf_calls.test.js @@ -1,4 +1,3 @@ -// FS is used to load samples from file. const fs = require('fs'); const electron = require('electron'); @@ -469,3 +468,54 @@ test('Test updateLoader function', () => { }, ); }); + +test('Test loadSchemaFromFile function with successful file load', () => { + const loadSchemaFromFile = sfcalls.__get__('loadSchemaFromFile'); + const setwindow = sfcalls.__get__('setwindow'); + + // Set the window first + setwindow(electron.mainWindow); + + // Call the function + loadSchemaFromFile(); + + // Verify dialog was shown with correct options + expect(electron.dialog.showOpenDialog).toHaveBeenCalledWith( + electron.mainWindow, + expect.objectContaining({ + title: 'Load Schema', + properties: ['openFile'], + }), + ); + + // Verify the correct messages were sent + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'log_message', + expect.objectContaining({ + sender: expect.stringContaining('Test Title'), + channel: 'Info', + message: expect.stringContaining('Test Message'), + }), + ); +}); + +test('Test loadSchemaFromFile function with file read error', () => { + const loadSchemaFromFile = sfcalls.__get__('loadSchemaFromFile'); + const setwindow = sfcalls.__get__('setwindow'); + + // Set the window first + setwindow(electron.mainWindow); + + // Call the function + loadSchemaFromFile(); + + // Verify error message was sent + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'log_message', + expect.objectContaining({ + sender: expect.stringContaining('Test Title'), + channel: 'Info', + message: expect.stringContaining('Test Message'), + }), + ); +}); From 7c7d8238605ba4c6fd193d7fcf9f4799ce4bd3e0 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sat, 14 Mar 2026 18:15:08 -0400 Subject: [PATCH 06/16] Setup main project file for better testing. --- src/tests/__mocks__/electron.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/tests/__mocks__/electron.js b/src/tests/__mocks__/electron.js index ec52684..d8f4e67 100644 --- a/src/tests/__mocks__/electron.js +++ b/src/tests/__mocks__/electron.js @@ -1,9 +1,14 @@ const mockWindow = { webContents: { send: jest.fn(), + findInPage: jest.fn(), + stopFindInPage: jest.fn(), }, }; +const mockDialogOpenCanceled = { filePaths: [], canceled: true }; +const mockDialogSaveCanceled = { filePath: undefined, canceled: true }; + const mockDialog = { showOpenDialog: jest.fn().mockResolvedValue({ filePaths: ['src/tests/sampleSObjectDescribes.json'], @@ -22,11 +27,20 @@ module.exports = { getAppPath: jest.fn().mockReturnValue('/app/path'), getPath: jest.fn().mockReturnValue('/path'), getName: jest.fn().mockReturnValue('appName'), + isPackaged: false, + }, + screen: { + getPrimaryDisplay: jest.fn().mockReturnValue({ + workAreaSize: { width: 1280, height: 800 }, + }), }, BrowserWindow: jest.fn().mockReturnValue(mockWindow), dialog: mockDialog, + mockDialogOpenCanceled, + mockDialogSaveCanceled, ipcMain: { on: jest.fn(), + handle: jest.fn(), send: jest.fn(), }, mainWindow: mockWindow, From a591a3dd85faf3a5ac6ed936cd6cecf10fdf67d0 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sat, 14 Mar 2026 18:50:36 -0400 Subject: [PATCH 07/16] Adding tests for sf_calls. --- src/tests/__mocks__/jsforce.js | 4 +- src/tests/sf_calls.test.js | 112 +++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/tests/__mocks__/jsforce.js b/src/tests/__mocks__/jsforce.js index 29ca895..4280cea 100644 --- a/src/tests/__mocks__/jsforce.js +++ b/src/tests/__mocks__/jsforce.js @@ -1,7 +1,7 @@ const jsforce = { Connection: jest.fn().mockImplementation(() => ({ - login: jest.fn().mockResolvedValue({}), - logout: jest.fn().mockResolvedValue({}), + login: jest.fn().mockResolvedValue({ organizationId: 'testOrgId', id: 'testUserId' }), + logout: Promise.resolve({}), sobject: jest.fn().mockReturnValue({ describe: jest.fn().mockResolvedValue({}), select: jest.fn().mockReturnThis(), diff --git a/src/tests/sf_calls.test.js b/src/tests/sf_calls.test.js index c45b64c..ec9981d 100644 --- a/src/tests/sf_calls.test.js +++ b/src/tests/sf_calls.test.js @@ -1,5 +1,6 @@ const fs = require('fs'); const electron = require('electron'); +const jsforce = require('jsforce'); // The actual module we're testing. const sfcalls = require('../sf_calls'); @@ -519,3 +520,114 @@ test('Test loadSchemaFromFile function with file read error', () => { }), ); }); + +test('Test sf_login success path', async () => { + jest.clearAllMocks(); + sfcalls.setwindow(electron.mainWindow); + + const mockEvent = { sender: { getTitle: jest.fn().mockReturnValue('Test Window') } }; + const mockArgs = { + url: 'https://test.salesforce.com', + username: 'test@test.com', + password: 'testpassword', + token: 'testtoken', + }; + + sfcalls.handlers.sf_login(mockEvent, mockArgs); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_login', + expect.objectContaining({ + status: true, + message: 'Login Successful', + }), + ); + // Password and token must be cleared before sending back to the renderer. + expect(mockArgs.password).toBe(''); + expect(mockArgs.token).toBe(''); +}); + +test('Test sf_login auth failure path', async () => { + jest.clearAllMocks(); + jsforce.Connection.mockImplementationOnce(() => ({ + login: jest.fn().mockRejectedValue(new Error('INVALID_LOGIN: Invalid username, password, security token')), + limitInfo: {}, + })); + sfcalls.setwindow(electron.mainWindow); + + const mockEvent = { sender: { getTitle: jest.fn().mockReturnValue('Test Window') } }; + const mockArgs = { + url: 'https://test.salesforce.com', + username: 'wrong@test.com', + password: 'wrongpassword', + token: '', + }; + + sfcalls.handlers.sf_login(mockEvent, mockArgs); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_login', + expect.objectContaining({ + status: false, + message: 'Login Failed', + }), + ); +}); + +test('Test sf_logout success path', async () => { + jest.clearAllMocks(); + sfcalls.__set__('sfConnections', { + testOrgId: { + instanceUrl: 'https://test.salesforce.com', + accessToken: 'testToken', + version: '63.0', + }, + }); + sfcalls.setwindow(electron.mainWindow); + + const mockEvent = { sender: { getTitle: jest.fn().mockReturnValue('Test Window') } }; + const mockArgs = { org: 'testOrgId' }; + + sfcalls.handlers.sf_logout(mockEvent, mockArgs); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_logout', + expect.objectContaining({ + status: true, + message: 'Logout Successful', + }), + ); + expect(sfcalls.__get__('sfConnections').testOrgId).toBeNull(); +}); + +test('Test sf_logout error path', () => { + jest.clearAllMocks(); + jsforce.Connection.mockImplementationOnce(() => ({ + logout: { then: (_onFulfilled, onRejected) => onRejected(new Error('Logout requested for unknown user')) }, + limitInfo: {}, + })); + sfcalls.__set__('sfConnections', { + errorOrgId: { + instanceUrl: 'https://test.salesforce.com', + accessToken: 'testToken', + version: '63.0', + }, + }); + sfcalls.setwindow(electron.mainWindow); + + const mockEvent = { sender: { getTitle: jest.fn().mockReturnValue('Test Window') } }; + const mockArgs = { org: 'errorOrgId' }; + + sfcalls.handlers.sf_logout(mockEvent, mockArgs); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_logout', + expect.objectContaining({ + status: false, + message: 'Logout Failed', + }), + ); +}); From 01c20fc670a51871c947c5e00f8ddacf6543931e Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sat, 14 Mar 2026 18:54:48 -0400 Subject: [PATCH 08/16] Expanding sf_calls testing --- src/tests/sf_calls.test.js | 165 +++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/src/tests/sf_calls.test.js b/src/tests/sf_calls.test.js index ec9981d..5f7d15b 100644 --- a/src/tests/sf_calls.test.js +++ b/src/tests/sf_calls.test.js @@ -631,3 +631,168 @@ test('Test sf_logout error path', () => { }), ); }); + +test('Test sf_describeGlobal success path', async () => { + jest.clearAllMocks(); + sfcalls.__set__('sfConnections', { + testOrgId: { + instanceUrl: 'https://test.salesforce.com', + accessToken: 'testToken', + version: '63.0', + }, + }); + sfcalls.setwindow(electron.mainWindow); + sfcalls.setPreferences(samplePrefs); + + const mockEvent = { sender: electron.mainWindow.webContents }; + const mockArgs = { org: 'testOrgId' }; + + sfcalls.handlers.sf_describeGlobal(mockEvent, mockArgs); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_list_objects', + expect.objectContaining({ + status: true, + message: 'Describe Global Successful', + response: expect.objectContaining({ + sobjects: [], + recommended: expect.any(Array), + }), + }), + ); +}); + +test('Test sf_describeGlobal error path', async () => { + jest.clearAllMocks(); + jsforce.Connection.mockImplementationOnce(() => ({ + describeGlobal: jest.fn().mockRejectedValue(new Error('Connection timed out')), + limitInfo: {}, + })); + sfcalls.__set__('sfConnections', { + errorOrgId: { + instanceUrl: 'https://test.salesforce.com', + accessToken: 'testToken', + version: '63.0', + }, + }); + sfcalls.setwindow(electron.mainWindow); + + const mockEvent = { sender: electron.mainWindow.webContents }; + const mockArgs = { org: 'errorOrgId' }; + + sfcalls.handlers.sf_describeGlobal(mockEvent, mockArgs); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_error', + expect.objectContaining({ + status: false, + message: 'Describe Global Failed', + }), + ); +}); + +test('Test sf_getObjectFields success path', async () => { + jest.clearAllMocks(); + const mockFields = [ + { + name: 'Id', + type: 'id', + length: 18, + label: 'Account ID', + calculated: false, + updateable: false, + createable: false, + defaultValue: null, + externalId: false, + picklistValues: [], + }, + { + name: 'Name', + type: 'string', + length: 80, + label: 'Account Name', + calculated: false, + updateable: true, + createable: true, + defaultValue: null, + externalId: false, + picklistValues: [], + }, + ]; + + jsforce.Connection.mockImplementationOnce(() => ({ + sobject: jest.fn().mockReturnValue({ + describe: jest.fn().mockResolvedValue({ name: 'Account', fields: mockFields }), + }), + limitInfo: {}, + })); + sfcalls.__set__('sfConnections', { + testOrgId: { + instanceUrl: 'https://test.salesforce.com', + accessToken: 'testToken', + version: '63.0', + }, + }); + sfcalls.setwindow(electron.mainWindow); + sfcalls.setPreferences(samplePrefs); + + const mockEvent = { sender: electron.mainWindow.webContents }; + const mockArgs = { org: 'testOrgId', objects: ['Account'] }; + + sfcalls.handlers.sf_getObjectFields(mockEvent, mockArgs); + await new Promise((resolve) => { process.nextTick(resolve); }); + + // proposedSchema should have been populated with the Account fields. + const schema = sfcalls.__get__('proposedSchema'); + expect(schema).toHaveProperty('Account'); + expect(schema.Account).toHaveProperty('Id'); + expect(schema.Account).toHaveProperty('Name'); + + // response_schema should have been sent to the renderer. + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_schema', + expect.objectContaining({ + status: false, + message: 'Processed Objects', + response: expect.objectContaining({ + schema: expect.objectContaining({ Account: expect.any(Object) }), + }), + }), + ); +}); + +test('Test sf_getObjectFields error path', async () => { + jest.clearAllMocks(); + jsforce.Connection.mockImplementationOnce(() => ({ + sobject: jest.fn().mockReturnValue({ + describe: jest.fn().mockRejectedValue(new Error('Object not found')), + }), + limitInfo: {}, + })); + sfcalls.__set__('sfConnections', { + testOrgId: { + instanceUrl: 'https://test.salesforce.com', + accessToken: 'testToken', + version: '63.0', + }, + }); + sfcalls.setwindow(electron.mainWindow); + sfcalls.setPreferences(samplePrefs); + + const mockEvent = { sender: electron.mainWindow.webContents }; + const mockArgs = { org: 'testOrgId', objects: ['NonExistentObject__c'] }; + + sfcalls.handlers.sf_getObjectFields(mockEvent, mockArgs); + await new Promise((resolve) => { process.nextTick(resolve); }); + + // An error log message should have been sent for the failed describe. + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'log_message', + expect.objectContaining({ + channel: 'Error', + message: expect.stringContaining('NonExistentObject__c'), + }), + ); +}); From ee8d7df3e6d6c5784511e634ebb148ec5d33280c Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sat, 14 Mar 2026 19:11:34 -0400 Subject: [PATCH 09/16] Expanded SF Calls test. Starts find.text.js --- src/tests/find.test.js | 87 ++++++++++++ src/tests/sf_calls.test.js | 262 +++++++++++++++++++++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 src/tests/find.test.js diff --git a/src/tests/find.test.js b/src/tests/find.test.js new file mode 100644 index 0000000..0fbc0d4 --- /dev/null +++ b/src/tests/find.test.js @@ -0,0 +1,87 @@ +const electron = require('electron'); // eslint-disable-line no-unused-vars +const find = require('../find'); + +// find.js holds module-level state (currentSearchText, searchEnabled). +// Reset it before each test via babel-plugin-rewire so tests are independent. +beforeEach(() => { + find.__set__('currentSearchText', undefined); + find.__set__('searchEnabled', false); +}); + +// Creates a pageContent stub with the methods used by find.js. +const makePageContent = () => ({ + on: jest.fn(), + send: jest.fn(), + findInPage: jest.fn(), + stopFindInPage: jest.fn(), +}); + +test('jumpToFind sends start_find to the focused window', () => { + const focusedWindow = { webContents: { send: jest.fn() } }; + find.jumpToFind(null, focusedWindow); + expect(focusedWindow.webContents.send).toHaveBeenCalledWith('start_find'); +}); + +test('enableSearch registers a found-in-page listener on first executeSearch call', () => { + const pageContent = makePageContent(); + find.executeSearch(pageContent, 'test', 'forward'); + expect(pageContent.on).toHaveBeenCalledWith('found-in-page', expect.any(Function)); + expect(find.__get__('searchEnabled')).toBe(true); +}); + +test('enableSearch found-in-page callback sends log_message with match count and search text', () => { + const pageContent = makePageContent(); + find.executeSearch(pageContent, 'test', 'forward'); + + // Capture and invoke the registered found-in-page callback. + const [, foundCallback] = pageContent.on.mock.calls.find(([evt]) => evt === 'found-in-page'); + foundCallback({}, { matches: 3 }); + + expect(pageContent.send).toHaveBeenCalledWith( + 'log_message', + expect.objectContaining({ + sender: 'Find', + channel: 'Info', + message: expect.stringContaining('test'), + }), + ); +}); + +test('executeSearch calls findInPage with forward:true and findNext:false for new search text', () => { + const pageContent = makePageContent(); + find.executeSearch(pageContent, 'hello', 'forward'); + expect(pageContent.findInPage).toHaveBeenCalledWith('hello', { + forward: true, + findNext: false, + matchCase: true, + }); +}); + +test('executeSearch calls findInPage with forward:false and findNext:false for new search text', () => { + const pageContent = makePageContent(); + find.executeSearch(pageContent, 'world', 'backward'); + expect(pageContent.findInPage).toHaveBeenCalledWith('world', { + forward: false, + findNext: false, + matchCase: true, + }); +}); + +test('executeSearch calls findInPage with findNext:true when the same text is searched again', () => { + const pageContent = makePageContent(); + find.executeSearch(pageContent, 'hello', 'forward'); + find.executeSearch(pageContent, 'hello', 'backward'); + expect(pageContent.findInPage).toHaveBeenLastCalledWith('hello', { + forward: false, + findNext: true, + matchCase: true, + }); +}); + +test('enableSearch is only called once across multiple executeSearch calls', () => { + const pageContent = makePageContent(); + find.executeSearch(pageContent, 'first', 'forward'); + find.executeSearch(pageContent, 'second', 'forward'); + // pageContent.on should be called only once (from the first enableSearch invocation). + expect(pageContent.on).toHaveBeenCalledTimes(1); +}); diff --git a/src/tests/sf_calls.test.js b/src/tests/sf_calls.test.js index 5f7d15b..21a9460 100644 --- a/src/tests/sf_calls.test.js +++ b/src/tests/sf_calls.test.js @@ -796,3 +796,265 @@ test('Test sf_getObjectFields error path', async () => { }), ); }); + +// ========================================== +// validateConnection tests +// ========================================== + +test('Test validateConnection success path', async () => { + const validateConnection = sfcalls.__get__('validateConnection'); + const mockKnexDb = { + raw: jest.fn().mockResolvedValue([{ isUp: 1 }]), + }; + + await expect(validateConnection(mockKnexDb)).resolves.toEqual([{ isUp: 1 }]); + expect(mockKnexDb.raw).toHaveBeenCalledWith('SELECT 1 AS isUp'); +}); + +test('Test validateConnection DB error path', async () => { + const validateConnection = sfcalls.__get__('validateConnection'); + const mockKnexDb = { + raw: jest.fn().mockRejectedValue(new Error('connection refused')), + }; + + await expect(validateConnection(mockKnexDb)).rejects.toThrow('connection refused'); + expect(mockKnexDb.raw).toHaveBeenCalledWith('SELECT 1 AS isUp'); +}); + +// ========================================== +// buildTable tests +// ========================================== + +// Preferences for buildTable unit tests — all indexes and SF-defaults disabled. +const buildTablePrefs = { + theme: 'Cyborg', + indexes: { + externalIds: false, + lookups: false, + picklists: false, + }, + picklists: { + type: 'enum', + unrestricted: false, + ensureBlanks: false, + }, + lookups: { + type: 'char(18)', + }, + defaults: { + attemptSFValues: false, + textEmptyString: false, + suppressReadOnly: false, + suppressAudit: false, + checkboxDefault: false, + }, +}; + +// Restores typeResolverBases fields that earlier tests mutate without reverting. +// resolveFieldType mutates the shared object but has no restore path, so we fix +// it before tests that require the canonical picklist→enum and reference→reference mappings. +const resetTypeResolver = () => { + const typeResolverBases = sfcalls.__get__('typeResolverBases'); + typeResolverBases.picklist = 'enum'; + typeResolverBases.reference = 'reference'; +}; + +// Creates a minimal knex column stub with the methods buildTable may call. +const makeColumnMock = () => ({ + collate: jest.fn().mockReturnThis(), + defaultTo: jest.fn().mockReturnThis(), + index: jest.fn().mockReturnThis(), +}); + +// Creates a full knex table stub whose column methods each return a fresh column mock. +const makeTableMock = (tableName) => ({ + _tableName: tableName, + binary: jest.fn().mockReturnValue(makeColumnMock()), + boolean: jest.fn().mockReturnValue(makeColumnMock()), + biginteger: jest.fn().mockReturnValue(makeColumnMock()), + date: jest.fn().mockReturnValue(makeColumnMock()), + datetime: jest.fn().mockReturnValue(makeColumnMock()), + decimal: jest.fn().mockReturnValue(makeColumnMock()), + enu: jest.fn().mockReturnValue(makeColumnMock()), + float: jest.fn().mockReturnValue(makeColumnMock()), + integer: jest.fn().mockReturnValue(makeColumnMock()), + string: jest.fn().mockReturnValue(makeColumnMock()), + text: jest.fn().mockReturnValue(makeColumnMock()), + time: jest.fn().mockReturnValue(makeColumnMock()), +}); + +test('Test buildTable invokes the correct column method for each field type', () => { + const buildTable = sfcalls.__get__('buildTable'); + resetTypeResolver(); + sfcalls.setPreferences(buildTablePrefs); + sfcalls.__set__('proposedSchema', { + TestObject: { + BinaryFld: { name: 'BinaryFld', type: 'byte', size: 8, defaultValue: null, externalId: false }, + BoolFld: { name: 'BoolFld', type: 'boolean', size: 0, defaultValue: null, externalId: false }, + BigIntFld: { name: 'BigIntFld', type: 'long', size: 18, defaultValue: null, externalId: false }, + DateFld: { name: 'DateFld', type: 'date', size: 10, defaultValue: null, externalId: false }, + DatetimeFld: { name: 'DatetimeFld', type: 'datetime', size: 10, defaultValue: null, externalId: false }, + DecimalFld: { + name: 'DecimalFld', type: 'double', size: 18, precision: 15, scale: 2, defaultValue: null, externalId: false, + }, + EnumFld: { + name: 'EnumFld', type: 'picklist', size: 255, defaultValue: null, externalId: false, values: ['A', 'B'], isRestricted: true, + }, + IntFld: { name: 'IntFld', type: 'int', size: 4, defaultValue: null, externalId: false }, + RefFld: { name: 'RefFld', type: 'reference', size: 18, defaultValue: null, externalId: false }, + TextFld: { name: 'TextFld', type: 'textarea', size: 1000, defaultValue: null, externalId: false }, + TimeFld: { name: 'TimeFld', type: 'time', size: 10, defaultValue: null, externalId: false }, + StrFld: { name: 'StrFld', type: 'string', size: 100, defaultValue: null, externalId: false }, + }, + }); + + const table = makeTableMock('TestObject'); + buildTable(table); + + expect(table.binary).toHaveBeenCalledWith('BinaryFld', 8); + expect(table.boolean).toHaveBeenCalledWith('BoolFld'); + expect(table.biginteger).toHaveBeenCalledWith('BigIntFld'); + expect(table.date).toHaveBeenCalledWith('DateFld'); + expect(table.datetime).toHaveBeenCalledWith('DatetimeFld'); + expect(table.decimal).toHaveBeenCalledWith('DecimalFld', 15, 2); + expect(table.enu).toHaveBeenCalledWith('EnumFld', ['A', 'B']); + expect(table.integer).toHaveBeenCalledWith('IntFld'); + expect(table.string).toHaveBeenCalledWith('RefFld', 18); + expect(table.string).toHaveBeenCalledWith('StrFld', 100); + expect(table.text).toHaveBeenCalledWith('TextFld'); + expect(table.time).toHaveBeenCalledWith('TimeFld'); +}); + +test('Test buildTable calls collate on reference-type fields', () => { + const buildTable = sfcalls.__get__('buildTable'); + resetTypeResolver(); + sfcalls.setPreferences(buildTablePrefs); + const refColMock = makeColumnMock(); + sfcalls.__set__('proposedSchema', { + Contact: { + AccountId: { + name: 'AccountId', type: 'reference', size: 18, defaultValue: null, externalId: false, + }, + }, + }); + + const table = makeTableMock('Contact'); + table.string = jest.fn().mockReturnValue(refColMock); + + buildTable(table); + + expect(table.string).toHaveBeenCalledWith('AccountId', 18); + expect(refColMock.collate).toHaveBeenCalledWith('utf8mb4_bin'); +}); + +test('Test buildTable creates index for externalId fields', () => { + const buildTable = sfcalls.__get__('buildTable'); + sfcalls.setPreferences({ + ...buildTablePrefs, + indexes: { externalIds: true, lookups: false, picklists: false }, + }); + + const colMock = makeColumnMock(); + sfcalls.__set__('proposedSchema', { + Account: { + External_ID__c: { + name: 'External_ID__c', type: 'string', size: 36, defaultValue: null, externalId: true, + }, + }, + }); + + const table = makeTableMock('Account'); + table.string = jest.fn().mockReturnValue(colMock); + + buildTable(table); + + expect(colMock.index).toHaveBeenCalledWith('Account_External_ID__c'); +}); + +test('Test buildTable creates index for lookup (reference) fields', () => { + const buildTable = sfcalls.__get__('buildTable'); + sfcalls.setPreferences({ + ...buildTablePrefs, + indexes: { externalIds: false, lookups: true, picklists: false }, + }); + + const colMock = makeColumnMock(); + sfcalls.__set__('proposedSchema', { + Contact: { + AccountId: { + name: 'AccountId', type: 'reference', size: 18, defaultValue: null, externalId: false, + }, + }, + }); + + const table = makeTableMock('Contact'); + table.string = jest.fn().mockReturnValue(colMock); + + buildTable(table); + + expect(colMock.index).toHaveBeenCalledWith('Contact_AccountId'); +}); + +test('Test buildTable creates index for picklist fields', () => { + const buildTable = sfcalls.__get__('buildTable'); + sfcalls.setPreferences({ + ...buildTablePrefs, + indexes: { externalIds: false, lookups: false, picklists: true }, + }); + + const colMock = makeColumnMock(); + sfcalls.__set__('proposedSchema', { + Opportunity: { + StageName: { + name: 'StageName', type: 'picklist', size: 40, defaultValue: null, externalId: false, values: ['Prospecting', 'Closed Won'], isRestricted: true, + }, + }, + }); + + const table = makeTableMock('Opportunity'); + // Override both enu and string: if the type resolver has been mutated by an earlier + // test, picklist may resolve to 'string' (default branch) instead of 'enum'. Either + // way the field is still indexed because addIndex checks field.type, not fieldType. + table.enu = jest.fn().mockReturnValue(colMock); + table.string = jest.fn().mockReturnValue(colMock); + + buildTable(table); + + expect(colMock.index).toHaveBeenCalledWith('Opportunity_StageName'); +}); + +test('Test buildTable does NOT create index when index preferences are disabled', () => { + const buildTable = sfcalls.__get__('buildTable'); + sfcalls.setPreferences(buildTablePrefs); + + const colMock = makeColumnMock(); + sfcalls.__set__('proposedSchema', { + Account: { + ExtId: { name: 'ExtId', type: 'string', size: 36, defaultValue: null, externalId: true }, + AccountId: { name: 'AccountId', type: 'reference', size: 18, defaultValue: null, externalId: false }, + StageName: { + name: 'StageName', type: 'picklist', size: 40, defaultValue: null, externalId: false, values: ['Open'], isRestricted: true, + }, + }, + }); + + const table = { + _tableName: 'Account', + binary: jest.fn().mockReturnValue(colMock), + boolean: jest.fn().mockReturnValue(colMock), + biginteger: jest.fn().mockReturnValue(colMock), + date: jest.fn().mockReturnValue(colMock), + datetime: jest.fn().mockReturnValue(colMock), + decimal: jest.fn().mockReturnValue(colMock), + enu: jest.fn().mockReturnValue(colMock), + float: jest.fn().mockReturnValue(colMock), + integer: jest.fn().mockReturnValue(colMock), + string: jest.fn().mockReturnValue(colMock), + text: jest.fn().mockReturnValue(colMock), + time: jest.fn().mockReturnValue(colMock), + }; + + buildTable(table); + + expect(colMock.index).not.toHaveBeenCalled(); +}); From 0793ed831855a6a3e2e0c129dc0011e4df9c0c62 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sat, 14 Mar 2026 19:23:21 -0400 Subject: [PATCH 10/16] more sf calls expansion --- src/tests/sf_calls.test.js | 164 +++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/src/tests/sf_calls.test.js b/src/tests/sf_calls.test.js index 21a9460..9f9be0e 100644 --- a/src/tests/sf_calls.test.js +++ b/src/tests/sf_calls.test.js @@ -1058,3 +1058,167 @@ test('Test buildTable does NOT create index when index preferences are disabled' expect(colMock.index).not.toHaveBeenCalled(); }); + +// ========================================== +// buildDatabase / knex_schema tests +// ========================================== + +test('Test buildDatabase success path sends response_db_generated', async () => { + jest.clearAllMocks(); + sfcalls.setwindow(electron.mainWindow); + sfcalls.setPreferences(buildTablePrefs); + resetTypeResolver(); + + sfcalls.__set__('proposedSchema', { + Account: { + Id: { name: 'Id', type: 'id', size: 18, defaultValue: null, externalId: false }, + Name: { name: 'Name', type: 'string', size: 255, defaultValue: null, externalId: false }, + }, + }); + + const mockSchema = { createTable: jest.fn().mockResolvedValue(true) }; + const mockDb = { schema: mockSchema }; + sfcalls.__set__('createKnexConnection', jest.fn().mockReturnValue(mockDb)); + sfcalls.__set__('validateConnection', jest.fn().mockResolvedValue([{ isUp: 1 }])); + + const mockEvent = { sender: electron.mainWindow.webContents }; + sfcalls.handlers.knex_schema(mockEvent, { + type: 'mysql', host: 'localhost', username: 'u', password: 'p', dbname: 'db', port: 3306, + }); + + await new Promise((resolve) => { process.nextTick(resolve); }); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_db_generated', + expect.objectContaining({ + status: true, + message: 'Database created', + responses: { Account: true }, + }), + ); +}); + +test('Test buildDatabase ER_TOO_BIG_ROWSIZE retry converts string fields and succeeds', async () => { + jest.clearAllMocks(); + sfcalls.setwindow(electron.mainWindow); + sfcalls.setPreferences(buildTablePrefs); + resetTypeResolver(); + + sfcalls.__set__('proposedSchema', { + BigTable: { + Field1: { name: 'Field1', type: 'string', size: 255, defaultValue: null, externalId: false }, + Field2: { name: 'Field2', type: 'phone', size: 40, defaultValue: null, externalId: false }, + }, + }); + + const rowsizeError = { + code: 'ER_TOO_BIG_ROWSIZE', message: 'Row size too large', errno: 1118, sql: '', + }; + const createTableMock = jest.fn() + .mockRejectedValueOnce(rowsizeError) + .mockResolvedValueOnce(true); + const mockSchema = { createTable: createTableMock }; + const mockDb = { schema: mockSchema }; + sfcalls.__set__('createKnexConnection', jest.fn().mockReturnValue(mockDb)); + sfcalls.__set__('validateConnection', jest.fn().mockResolvedValue([{ isUp: 1 }])); + + const mockEvent = { sender: electron.mainWindow.webContents }; + sfcalls.handlers.knex_schema(mockEvent, { + type: 'mysql', host: 'localhost', username: 'u', password: 'p', dbname: 'db', port: 3306, + }); + + // Allow nested async retry chains to settle. + await new Promise((resolve) => { process.nextTick(resolve); }); + await new Promise((resolve) => { process.nextTick(resolve); }); + await new Promise((resolve) => { process.nextTick(resolve); }); + + // createTable should have been called twice (initial attempt + retry). + expect(createTableMock).toHaveBeenCalledTimes(2); + + // String-resolving fields should have been converted to text. + const schema = sfcalls.__get__('proposedSchema'); + expect(schema.BigTable.Field1.type).toBe('text'); + expect(schema.BigTable.Field2.type).toBe('text'); + + // After the retry succeeds, response_db_generated should report success. + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_db_generated', + expect.objectContaining({ + status: true, + message: 'Database created', + responses: { BigTable: true }, + }), + ); +}); + +test('Test buildDatabase ER_TOO_MANY_KEYS marks table success and sends response_db_generated', async () => { + jest.clearAllMocks(); + sfcalls.setwindow(electron.mainWindow); + sfcalls.setPreferences(buildTablePrefs); + resetTypeResolver(); + + sfcalls.__set__('proposedSchema', { + WideTable: { + Id: { name: 'Id', type: 'id', size: 18, defaultValue: null, externalId: false }, + }, + }); + + const tooManyKeysError = { + code: 'ER_TOO_MANY_KEYS', message: 'Too many keys', errno: 1069, sql: '', + }; + const mockSchema = { createTable: jest.fn().mockRejectedValue(tooManyKeysError) }; + const mockDb = { schema: mockSchema }; + sfcalls.__set__('createKnexConnection', jest.fn().mockReturnValue(mockDb)); + sfcalls.__set__('validateConnection', jest.fn().mockResolvedValue([{ isUp: 1 }])); + + const mockEvent = { sender: electron.mainWindow.webContents }; + sfcalls.handlers.knex_schema(mockEvent, { + type: 'mysql', host: 'localhost', username: 'u', password: 'p', dbname: 'db', port: 3306, + }); + + await new Promise((resolve) => { process.nextTick(resolve); }); + await new Promise((resolve) => { process.nextTick(resolve); }); + + // ER_TOO_MANY_KEYS marks the table successful (table was created, just missing some indexes). + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_db_generated', + expect.objectContaining({ + status: true, + message: 'Database created', + responses: { WideTable: true }, + }), + ); +}); + +test('Test buildDatabase connection failure sends response_db_generated with status false', async () => { + jest.clearAllMocks(); + sfcalls.setwindow(electron.mainWindow); + sfcalls.setPreferences(buildTablePrefs); + + sfcalls.__set__('proposedSchema', { + Account: { + Id: { name: 'Id', type: 'id', size: 18, defaultValue: null, externalId: false }, + }, + }); + + const mockDb = { schema: {} }; + sfcalls.__set__('createKnexConnection', jest.fn().mockReturnValue(mockDb)); + sfcalls.__set__('validateConnection', jest.fn().mockRejectedValue(new Error('connection refused'))); + + const mockEvent = { sender: electron.mainWindow.webContents }; + sfcalls.handlers.knex_schema(mockEvent, { + type: 'mysql', host: 'localhost', username: 'u', password: 'p', dbname: 'db', port: 3306, + }); + + await new Promise((resolve) => { process.nextTick(resolve); }); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_db_generated', + expect.objectContaining({ + status: false, + message: expect.stringContaining('connection refused'), + }), + ); +}); From d7f6f6be2e73488a3ff4070484cfc297b51f3afa Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sat, 14 Mar 2026 21:14:21 -0400 Subject: [PATCH 11/16] Further improvements to sf_calls and tests. --- src/sf_calls.js | 10 +- src/tests/sf_calls.test.js | 253 ++++++++++++++++++++++++++++++++++--- 2 files changed, 241 insertions(+), 22 deletions(-) diff --git a/src/sf_calls.js b/src/sf_calls.js index cd2db1a..a3634b0 100644 --- a/src/sf_calls.js +++ b/src/sf_calls.js @@ -195,9 +195,13 @@ const loadSchemaFromFile = () => { return; } - // @TODO: Validate that schema is in a useable form. - - proposedSchema = JSON.parse(data); + // @TODO: Further validate that schema is in a useable form. + try { + proposedSchema = JSON.parse(data); + } catch (parseErr) { + logMessage('File', 'Error', `Unable to parse schema file: ${parseErr.message}`); + return; + } logMessage('File', 'Info', `Loaded schema from file: ${fileName}`); // Send Schema to interface for review. diff --git a/src/tests/sf_calls.test.js b/src/tests/sf_calls.test.js index 9f9be0e..e0dc67a 100644 --- a/src/tests/sf_calls.test.js +++ b/src/tests/sf_calls.test.js @@ -470,17 +470,22 @@ test('Test updateLoader function', () => { ); }); -test('Test loadSchemaFromFile function with successful file load', () => { +// ========================================== +// loadSchemaFromFile tests +// ========================================== + +test('Test loadSchemaFromFile calls showOpenDialog with correct options', async () => { + jest.clearAllMocks(); const loadSchemaFromFile = sfcalls.__get__('loadSchemaFromFile'); - const setwindow = sfcalls.__get__('setwindow'); + sfcalls.setwindow(electron.mainWindow); - // Set the window first - setwindow(electron.mainWindow); + jest.spyOn(fs, 'readFile').mockImplementationOnce((_path, callback) => { + callback(null, Buffer.from(JSON.stringify({}))); + }); - // Call the function loadSchemaFromFile(); + await new Promise((resolve) => { process.nextTick(resolve); }); - // Verify dialog was shown with correct options expect(electron.dialog.showOpenDialog).toHaveBeenCalledWith( electron.mainWindow, expect.objectContaining({ @@ -488,37 +493,99 @@ test('Test loadSchemaFromFile function with successful file load', () => { properties: ['openFile'], }), ); +}); + +test('Test loadSchemaFromFile success path populates proposedSchema and sends response_schema', async () => { + jest.clearAllMocks(); + const loadSchemaFromFile = sfcalls.__get__('loadSchemaFromFile'); + sfcalls.setwindow(electron.mainWindow); + + const schemaData = { Account: { Id: { name: 'Id', type: 'id', size: 18 } } }; + jest.spyOn(fs, 'readFile').mockImplementationOnce((_path, callback) => { + callback(null, Buffer.from(JSON.stringify(schemaData))); + }); + + loadSchemaFromFile(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(sfcalls.__get__('proposedSchema')).toEqual(schemaData); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_schema', + expect.objectContaining({ + status: false, + response: expect.objectContaining({ schema: schemaData }), + }), + ); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'log_message', + expect.objectContaining({ sender: 'File', channel: 'Info' }), + ); +}); + +test('Test loadSchemaFromFile logs error when fs.readFile fails', async () => { + jest.clearAllMocks(); + const loadSchemaFromFile = sfcalls.__get__('loadSchemaFromFile'); + sfcalls.setwindow(electron.mainWindow); + + jest.spyOn(fs, 'readFile').mockImplementationOnce((_path, callback) => { + callback(new Error('ENOENT: file not found'), null); + }); + + loadSchemaFromFile(); + await new Promise((resolve) => { process.nextTick(resolve); }); - // Verify the correct messages were sent expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( 'log_message', expect.objectContaining({ - sender: expect.stringContaining('Test Title'), - channel: 'Info', - message: expect.stringContaining('Test Message'), + sender: 'File', + channel: 'Error', + message: expect.stringContaining('Unable to load'), }), ); }); -test('Test loadSchemaFromFile function with file read error', () => { +test('Test loadSchemaFromFile does nothing when dialog is canceled', async () => { + jest.clearAllMocks(); const loadSchemaFromFile = sfcalls.__get__('loadSchemaFromFile'); - const setwindow = sfcalls.__get__('setwindow'); + sfcalls.setwindow(electron.mainWindow); - // Set the window first - setwindow(electron.mainWindow); + electron.dialog.showOpenDialog.mockResolvedValueOnce(electron.mockDialogOpenCanceled); + const readFileSpy = jest.spyOn(fs, 'readFile'); - // Call the function loadSchemaFromFile(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(readFileSpy).not.toHaveBeenCalled(); + expect(electron.mainWindow.webContents.send).not.toHaveBeenCalled(); +}); + +test('Test loadSchemaFromFile logs error when file contains invalid JSON', async () => { + jest.clearAllMocks(); + const loadSchemaFromFile = sfcalls.__get__('loadSchemaFromFile'); + sfcalls.setwindow(electron.mainWindow); + + jest.spyOn(fs, 'readFile').mockImplementationOnce((_path, callback) => { + callback(null, Buffer.from('{ not valid json ')); + }); + + loadSchemaFromFile(); + await new Promise((resolve) => { process.nextTick(resolve); }); - // Verify error message was sent expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( 'log_message', expect.objectContaining({ - sender: expect.stringContaining('Test Title'), - channel: 'Info', - message: expect.stringContaining('Test Message'), + sender: 'File', + channel: 'Error', + message: expect.stringContaining('Unable to parse'), }), ); + + // response_schema must NOT be sent when parsing fails. + const calls = electron.mainWindow.webContents.send.mock.calls; + const responseSchemaCalls = calls.filter((c) => c[0] === 'response_schema'); + expect(responseSchemaCalls).toHaveLength(0); }); test('Test sf_login success path', async () => { @@ -1222,3 +1289,151 @@ test('Test buildDatabase connection failure sends response_db_generated with sta }), ); }); + +// ========================================== +// saveSchemaToFile tests +// ========================================== + +test('Test saveSchemaToFile calls showSaveDialog with correct options', async () => { + jest.clearAllMocks(); + const saveSchemaToFile = sfcalls.__get__('saveSchemaToFile'); + sfcalls.setwindow(electron.mainWindow); + + jest.spyOn(fs, 'writeFile').mockImplementationOnce((_path, _data, callback) => { + callback(null); + }); + + saveSchemaToFile(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.dialog.showSaveDialog).toHaveBeenCalledWith( + electron.mainWindow, + expect.objectContaining({ title: 'Save Schema To' }), + ); +}); + +test('Test saveSchemaToFile success path writes JSON and logs info', async () => { + jest.clearAllMocks(); + const saveSchemaToFile = sfcalls.__get__('saveSchemaToFile'); + sfcalls.setwindow(electron.mainWindow); + + const schemaData = { Contact: { Name: { name: 'Name', type: 'string', size: 80 } } }; + sfcalls.__set__('proposedSchema', schemaData); + + // Default mock returns 'path/to/save/file' (no .json extension — triggers auto-append). + const writeFileSpy = jest.spyOn(fs, 'writeFile').mockImplementationOnce((_path, _data, callback) => { + callback(null); + }); + + saveSchemaToFile(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + // The path written must end with .json. + expect(writeFileSpy).toHaveBeenCalledWith( + expect.stringMatching(/\.json$/), + JSON.stringify(schemaData), + expect.any(Function), + ); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'log_message', + expect.objectContaining({ sender: 'Save', channel: 'Info' }), + ); +}); + +test('Test saveSchemaToFile does not append .json when extension already present', async () => { + jest.clearAllMocks(); + const saveSchemaToFile = sfcalls.__get__('saveSchemaToFile'); + sfcalls.setwindow(electron.mainWindow); + + electron.dialog.showSaveDialog.mockResolvedValueOnce({ + filePath: '/path/to/schema.json', + canceled: false, + }); + + const writeFileSpy = jest.spyOn(fs, 'writeFile').mockImplementationOnce((_path, _data, callback) => { + callback(null); + }); + + saveSchemaToFile(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + // The path should be exactly as provided — no double extension. + expect(writeFileSpy).toHaveBeenCalledWith( + '/path/to/schema.json', + expect.any(String), + expect.any(Function), + ); +}); + +test('Test saveSchemaToFile does nothing when dialog is canceled', async () => { + jest.clearAllMocks(); + const saveSchemaToFile = sfcalls.__get__('saveSchemaToFile'); + sfcalls.setwindow(electron.mainWindow); + + electron.dialog.showSaveDialog.mockResolvedValueOnce(electron.mockDialogSaveCanceled); + const writeFileSpy = jest.spyOn(fs, 'writeFile'); + + saveSchemaToFile(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(writeFileSpy).not.toHaveBeenCalled(); + expect(electron.mainWindow.webContents.send).not.toHaveBeenCalled(); +}); + +test('Test saveSchemaToFile logs error when fs.writeFile fails', async () => { + jest.clearAllMocks(); + const saveSchemaToFile = sfcalls.__get__('saveSchemaToFile'); + sfcalls.setwindow(electron.mainWindow); + + jest.spyOn(fs, 'writeFile').mockImplementationOnce((_path, _data, callback) => { + callback(new Error('EACCES: permission denied')); + }); + + saveSchemaToFile(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'log_message', + expect.objectContaining({ + sender: 'Save', + channel: 'Error', + message: expect.stringContaining('Unable to save'), + }), + ); +}); + +// ========================================== +// IPC handler wrappers: load_schema / save_schema +// ========================================== + +test('Test load_schema handler delegates to loadSchemaFromFile', async () => { + jest.clearAllMocks(); + sfcalls.setwindow(electron.mainWindow); + + const mockLoad = jest.fn(); + sfcalls.__set__('loadSchemaFromFile', mockLoad); + + const mockEvent = { sender: electron.mainWindow.webContents }; + sfcalls.handlers.load_schema(mockEvent, {}); + + expect(mockLoad).toHaveBeenCalledTimes(1); + + // Restore the real implementation so later tests are unaffected. + sfcalls.__set__('loadSchemaFromFile', sfcalls.__get__('loadSchemaFromFile')); +}); + +test('Test save_schema handler delegates to saveSchemaToFile', async () => { + jest.clearAllMocks(); + sfcalls.setwindow(electron.mainWindow); + + const mockSave = jest.fn(); + sfcalls.__set__('saveSchemaToFile', mockSave); + + const mockEvent = { sender: electron.mainWindow.webContents }; + sfcalls.handlers.save_schema(mockEvent, {}); + + expect(mockSave).toHaveBeenCalledTimes(1); + + sfcalls.__set__('saveSchemaToFile', sfcalls.__get__('saveSchemaToFile')); +}); From 34a3205645468bdcd547d9c6ea6f781c9dcd98a2 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sat, 14 Mar 2026 21:16:09 -0400 Subject: [PATCH 12/16] Adding coverage for sqlite3 sf_calls --- src/tests/sf_calls.test.js | 149 +++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/src/tests/sf_calls.test.js b/src/tests/sf_calls.test.js index e0dc67a..e821f57 100644 --- a/src/tests/sf_calls.test.js +++ b/src/tests/sf_calls.test.js @@ -1437,3 +1437,152 @@ test('Test save_schema handler delegates to saveSchemaToFile', async () => { sfcalls.__set__('saveSchemaToFile', sfcalls.__get__('saveSchemaToFile')); }); + +// ========================================== +// saveSqlite3File tests +// ========================================== + +test('Test saveSqlite3File calls showSaveDialog with correct options', async () => { + jest.clearAllMocks(); + const saveSqlite3File = sfcalls.__get__('saveSqlite3File'); + sfcalls.setwindow(electron.mainWindow); + + // Default mock returns 'path/to/save/file' (no known extension — .sqlite will be appended). + saveSqlite3File(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.dialog.showSaveDialog).toHaveBeenCalledWith( + electron.mainWindow, + expect.objectContaining({ title: 'Select Sqlite3 Database Location' }), + ); +}); + +test('Test saveSqlite3File appends .sqlite when no recognized extension is given', async () => { + jest.clearAllMocks(); + const saveSqlite3File = sfcalls.__get__('saveSqlite3File'); + sfcalls.setwindow(electron.mainWindow); + + // Default mock gives 'path/to/save/file' — no sqlite/db/sqlite3 extension. + saveSqlite3File(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_sqlite3_file', + expect.objectContaining({ + status: false, + message: 'Sqlite3 File Selected', + response: { filePath: 'path/to/save/file.sqlite' }, + }), + ); +}); + +test('Test saveSqlite3File keeps .sqlite extension unchanged', async () => { + jest.clearAllMocks(); + const saveSqlite3File = sfcalls.__get__('saveSqlite3File'); + sfcalls.setwindow(electron.mainWindow); + + electron.dialog.showSaveDialog.mockResolvedValueOnce({ + filePath: '/data/mydb.sqlite', + canceled: false, + }); + + saveSqlite3File(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_sqlite3_file', + expect.objectContaining({ + response: { filePath: '/data/mydb.sqlite' }, + }), + ); +}); + +test('Test saveSqlite3File keeps .db extension unchanged', async () => { + jest.clearAllMocks(); + const saveSqlite3File = sfcalls.__get__('saveSqlite3File'); + sfcalls.setwindow(electron.mainWindow); + + electron.dialog.showSaveDialog.mockResolvedValueOnce({ + filePath: '/data/mydb.db', + canceled: false, + }); + + saveSqlite3File(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_sqlite3_file', + expect.objectContaining({ + response: { filePath: '/data/mydb.db' }, + }), + ); +}); + +test('Test saveSqlite3File keeps .sqlite3 extension unchanged', async () => { + jest.clearAllMocks(); + const saveSqlite3File = sfcalls.__get__('saveSqlite3File'); + sfcalls.setwindow(electron.mainWindow); + + electron.dialog.showSaveDialog.mockResolvedValueOnce({ + filePath: '/data/mydb.sqlite3', + canceled: false, + }); + + saveSqlite3File(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'response_sqlite3_file', + expect.objectContaining({ + response: { filePath: '/data/mydb.sqlite3' }, + }), + ); +}); + +test('Test saveSqlite3File does nothing when dialog is canceled', async () => { + jest.clearAllMocks(); + const saveSqlite3File = sfcalls.__get__('saveSqlite3File'); + sfcalls.setwindow(electron.mainWindow); + + electron.dialog.showSaveDialog.mockResolvedValueOnce(electron.mockDialogSaveCanceled); + + saveSqlite3File(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).not.toHaveBeenCalled(); +}); + +test('Test saveSqlite3File logs error when dialog rejects', async () => { + jest.clearAllMocks(); + const saveSqlite3File = sfcalls.__get__('saveSqlite3File'); + sfcalls.setwindow(electron.mainWindow); + + electron.dialog.showSaveDialog.mockRejectedValueOnce(new Error('dialog crashed')); + + saveSqlite3File(); + await new Promise((resolve) => { process.nextTick(resolve); }); + + expect(electron.mainWindow.webContents.send).toHaveBeenCalledWith( + 'log_message', + expect.objectContaining({ + sender: 'Save', + channel: 'Error', + message: expect.stringContaining('dialog crashed'), + }), + ); +}); + +test('Test select_sqlite3_location handler delegates to saveSqlite3File', () => { + jest.clearAllMocks(); + sfcalls.setwindow(electron.mainWindow); + + const mockFn = jest.fn(); + sfcalls.__set__('saveSqlite3File', mockFn); + + const mockEvent = { sender: electron.mainWindow.webContents }; + sfcalls.handlers.select_sqlite3_location(mockEvent, {}); + + expect(mockFn).toHaveBeenCalledTimes(1); + + sfcalls.__set__('saveSqlite3File', sfcalls.__get__('saveSqlite3File')); +}); From 2b075e27278903bfb2eccbf44846e2b1f07a72b1 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sat, 14 Mar 2026 21:26:50 -0400 Subject: [PATCH 13/16] Expansion of Render tests --- app/tests/render.test.js | 209 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/app/tests/render.test.js b/app/tests/render.test.js index 307f5c8..f7ba667 100644 --- a/app/tests/render.test.js +++ b/app/tests/render.test.js @@ -13,6 +13,8 @@ beforeAll(() => { }; // JSONViewer Mock global.$.fn.jsonViewer = jest.fn(); + // Bootstrap tab plugin stub (used by displayDraftSchema) + global.$.fn.tab = jest.fn(); }); beforeEach(() => { @@ -135,3 +137,210 @@ test('Test updateMessage', () => { // refreshObjectDisplay(); // expect('Test Stub').toEqual('Test Stub'); // }); + +// ---- fetchOrgUser ---- + +test('fetchOrgUser returns empty string for an unknown org id', () => { + const fetchOrgUser = render.__get__('fetchOrgUser'); + expect(fetchOrgUser('unknown-org')).toEqual(''); +}); + +test('fetchOrgUser returns the username text for a known org id', () => { + const fetchOrgUser = render.__get__('fetchOrgUser'); + const sel = document.getElementById('active-org'); + const opt = document.createElement('option'); + opt.value = 'abc123'; + opt.text = 'user@example.com'; + opt.id = 'sforg-abc123'; + sel.appendChild(opt); + expect(fetchOrgUser('abc123')).toEqual('user@example.com'); +}); + +// ---- handleLogin ---- + +test('handleLogin adds an option to the org dropdown, shows org-status, and enables fetch-objects', () => { + const handleLogin = render.__get__('handleLogin'); + const data = { + message: 'Welcome', + request: { username: 'admin@example.com' }, + response: { organizationId: 'org001' }, + }; + handleLogin(data); + + const opt = document.getElementById('sforg-org001'); + expect(opt).not.toBeNull(); + expect(opt.value).toEqual('org001'); + expect(document.getElementById('org-status').style.display).toEqual('block'); + expect(document.getElementById('btn-fetch-objects').disabled).toBe(false); +}); + +// ---- displayObjectList ---- + +test('displayObjectList renders one row per createable object', () => { + const displayObjectList = render.__get__('displayObjectList'); + const sObjects = [ + { name: 'Account', label: 'Account', createable: true }, + { name: 'Contact', label: 'Contact', createable: true }, + { name: 'Task', label: 'Task', createable: false }, + ]; + + displayObjectList('', sObjects, []); + + const tbody = document.getElementById('results-table').getElementsByTagName('tbody')[0]; + expect(tbody.rows.length).toEqual(2); +}); + +test('displayObjectList renders selected objects as the first rows when not pre-sorted', () => { + const displayObjectList = render.__get__('displayObjectList'); + const sObjects = [ + { name: 'Account', label: 'Account', createable: true }, + { name: 'Contact', label: 'Contact', createable: true }, + ]; + + displayObjectList('', sObjects, ['Contact']); + + const tbody = document.getElementById('results-table').getElementsByTagName('tbody')[0]; + const firstCheckbox = tbody.rows[0].cells[0].querySelector('input[type=checkbox]'); + expect(firstCheckbox.dataset.objectName).toEqual('Contact'); +}); + +// ---- sortObjectTable ---- + +test('sortObjectTable re-renders table rows in ascending label order', () => { + const displayObjectList = render.__get__('displayObjectList'); + const sortObjectTable = render.__get__('sortObjectTable'); + const sObjects = [ + { name: 'Zzz', label: 'Zzz', createable: true }, + { name: 'Aaa', label: 'Aaa', createable: true }, + ]; + + // Populate the table first so sortObjectTable has data-rowData to read. + displayObjectList('', sObjects, [], true, 'label', 'ASC'); + sortObjectTable('label', 'ASC'); + + const tbody = document.getElementById('results-table').getElementsByTagName('tbody')[0]; + // Column 0 = select checkbox; column 1 = label. + expect(tbody.rows[0].cells[1].textContent).toEqual('Aaa'); +}); + +test('sortObjectTable re-renders table rows in descending label order', () => { + const displayObjectList = render.__get__('displayObjectList'); + const sortObjectTable = render.__get__('sortObjectTable'); + const sObjects = [ + { name: 'Aaa', label: 'Aaa', createable: true }, + { name: 'Zzz', label: 'Zzz', createable: true }, + ]; + + displayObjectList('', sObjects, [], true, 'label', 'ASC'); + sortObjectTable('label', 'DESC'); + + const tbody = document.getElementById('results-table').getElementsByTagName('tbody')[0]; + expect(tbody.rows[0].cells[1].textContent).toEqual('Zzz'); +}); + +// ---- IPC receive callbacks ---- + +// Helper: retrieve the callback registered for a given channel via window.api.receive. +const getReceiveCallback = (channel) => { + const entry = window.api.receive.mock.calls.find(([ch]) => ch === channel); + return entry ? entry[1] : undefined; +}; + +test('response_login success path updates login message and enables fetch-objects button', () => { + const cb = getReceiveCallback('response_login'); + cb({ + status: true, + message: 'Login successful', + request: { username: 'admin@example.com' }, + response: { organizationId: 'org999' }, + }); + expect(document.getElementById('login-response-message').innerText).toEqual('Login successful'); + expect(document.getElementById('btn-fetch-objects').disabled).toBe(false); +}); + +test('response_login error path logs an error row and updates status message', () => { + const cb = getReceiveCallback('response_login'); + const logTable = document.getElementById('consoleMessageTable'); + const before = logTable.rows.length; + cb({ status: false, message: 'Invalid credentials', response: {} }); + expect(logTable.rows.length).toBeGreaterThan(before); + expect(document.getElementById('results-message-only').innerText).toEqual('Login Error'); +}); + +test('response_logout logs a message and updates the status text', () => { + const cb = getReceiveCallback('response_logout'); + const logTable = document.getElementById('consoleMessageTable'); + const before = logTable.rows.length; + cb({ message: 'Logged out', response: {} }); + expect(logTable.rows.length).toBeGreaterThan(before); + expect(document.getElementById('results-message-only').innerText) + .toEqual('Salesforce connection removed.'); +}); + +test('response_error logs an error row', () => { + const cb = getReceiveCallback('response_error'); + const logTable = document.getElementById('consoleMessageTable'); + const before = logTable.rows.length; + cb({ message: 'Something broke', response: 'Error details' }); + expect(logTable.rows.length).toBeGreaterThan(before); +}); + +test('response_list_objects success populates the object table with createable objects', () => { + const cb = getReceiveCallback('response_list_objects'); + cb({ + status: true, + request: { org: '' }, + response: { + sobjects: [ + { name: 'Account', label: 'Account', createable: true }, + { name: 'Lead', label: 'Lead', createable: true }, + { name: 'Activity', label: 'Activity', createable: false }, + ], + recommended: [], + }, + }); + const tbody = document.getElementById('results-table').getElementsByTagName('tbody')[0]; + expect(tbody.rows.length).toEqual(2); +}); + +test('response_list_objects error path logs an error row', () => { + const cb = getReceiveCallback('response_list_objects'); + const logTable = document.getElementById('consoleMessageTable'); + const before = logTable.rows.length; + cb({ status: false, request: {}, response: {} }); + expect(logTable.rows.length).toBeGreaterThan(before); +}); + +test('response_schema makes the object viewer visible and logs a success row', () => { + const cb = getReceiveCallback('response_schema'); + const logTable = document.getElementById('consoleMessageTable'); + const before = logTable.rows.length; + cb({ request: { org: '' }, response: { schema: { Account: {} } } }); + expect(document.getElementById('results-object-viewer-wrapper').style.display).toEqual('block'); + expect(logTable.rows.length).toBeGreaterThan(before); +}); + +test('response_db_generated full success logs completion and updates message', () => { + const cb = getReceiveCallback('response_db_generated'); + const logTable = document.getElementById('consoleMessageTable'); + const before = logTable.rows.length; + cb({ response: {}, responses: { Account: true, Contact: true } }); + expect(logTable.rows.length).toBeGreaterThan(before); + expect(document.getElementById('results-message-only').innerText) + .toEqual('Database creation complete, all tables created'); +}); + +test('response_db_generated full failure updates message to all-failed text', () => { + const cb = getReceiveCallback('response_db_generated'); + // Omit the 'response' key so hasResponses=false, which initialises fullFailure=true. + cb({ responses: { Account: false } }); + expect(document.getElementById('results-message-only').innerText) + .toEqual('Error creating database tables, all tables failed'); +}); + +test('response_db_generated partial success updates message with some-tables-had-error text', () => { + const cb = getReceiveCallback('response_db_generated'); + cb({ response: {}, responses: { Account: true, Contact: false } }); + expect(document.getElementById('results-message-only').innerText) + .toContain('some tables had error'); +}); From a294e299f067a45c244536e9fa1010e0b7e20be8 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sat, 14 Mar 2026 21:50:31 -0400 Subject: [PATCH 14/16] Adding lint fix command --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 5804334..739a402 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "start": "electron-forge start", "lint": "eslint src app --ignore-path .gitignore ", + "lint:fix": "eslint src app --ignore-path .gitignore --fix", "test": "jest --coverage --passWithNoTests", "test-on-commit": "jest", "prepare": "husky install", From 0e5fee27bc310b43203871cb0227fe34221e10e1 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sat, 14 Mar 2026 21:50:39 -0400 Subject: [PATCH 15/16] Auto fixes to linting --- app/tests/render.test.js | 4 +-- src/tests/sf_calls.test.js | 74 ++++++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/app/tests/render.test.js b/app/tests/render.test.js index f7ba667..d3a4e5f 100644 --- a/app/tests/render.test.js +++ b/app/tests/render.test.js @@ -187,7 +187,7 @@ test('displayObjectList renders one row per createable object', () => { displayObjectList('', sObjects, []); const tbody = document.getElementById('results-table').getElementsByTagName('tbody')[0]; - expect(tbody.rows.length).toEqual(2); + expect(tbody.rows).toHaveLength(2); }); test('displayObjectList renders selected objects as the first rows when not pre-sorted', () => { @@ -300,7 +300,7 @@ test('response_list_objects success populates the object table with createable o }, }); const tbody = document.getElementById('results-table').getElementsByTagName('tbody')[0]; - expect(tbody.rows.length).toEqual(2); + expect(tbody.rows).toHaveLength(2); }); test('response_list_objects error path logs an error row', () => { diff --git a/src/tests/sf_calls.test.js b/src/tests/sf_calls.test.js index e821f57..4a00d78 100644 --- a/src/tests/sf_calls.test.js +++ b/src/tests/sf_calls.test.js @@ -583,7 +583,7 @@ test('Test loadSchemaFromFile logs error when file contains invalid JSON', async ); // response_schema must NOT be sent when parsing fails. - const calls = electron.mainWindow.webContents.send.mock.calls; + const { calls } = electron.mainWindow.webContents.send.mock; const responseSchemaCalls = calls.filter((c) => c[0] === 'response_schema'); expect(responseSchemaCalls).toHaveLength(0); }); @@ -956,22 +956,42 @@ test('Test buildTable invokes the correct column method for each field type', () sfcalls.setPreferences(buildTablePrefs); sfcalls.__set__('proposedSchema', { TestObject: { - BinaryFld: { name: 'BinaryFld', type: 'byte', size: 8, defaultValue: null, externalId: false }, - BoolFld: { name: 'BoolFld', type: 'boolean', size: 0, defaultValue: null, externalId: false }, - BigIntFld: { name: 'BigIntFld', type: 'long', size: 18, defaultValue: null, externalId: false }, - DateFld: { name: 'DateFld', type: 'date', size: 10, defaultValue: null, externalId: false }, - DatetimeFld: { name: 'DatetimeFld', type: 'datetime', size: 10, defaultValue: null, externalId: false }, + BinaryFld: { + name: 'BinaryFld', type: 'byte', size: 8, defaultValue: null, externalId: false, + }, + BoolFld: { + name: 'BoolFld', type: 'boolean', size: 0, defaultValue: null, externalId: false, + }, + BigIntFld: { + name: 'BigIntFld', type: 'long', size: 18, defaultValue: null, externalId: false, + }, + DateFld: { + name: 'DateFld', type: 'date', size: 10, defaultValue: null, externalId: false, + }, + DatetimeFld: { + name: 'DatetimeFld', type: 'datetime', size: 10, defaultValue: null, externalId: false, + }, DecimalFld: { name: 'DecimalFld', type: 'double', size: 18, precision: 15, scale: 2, defaultValue: null, externalId: false, }, EnumFld: { name: 'EnumFld', type: 'picklist', size: 255, defaultValue: null, externalId: false, values: ['A', 'B'], isRestricted: true, }, - IntFld: { name: 'IntFld', type: 'int', size: 4, defaultValue: null, externalId: false }, - RefFld: { name: 'RefFld', type: 'reference', size: 18, defaultValue: null, externalId: false }, - TextFld: { name: 'TextFld', type: 'textarea', size: 1000, defaultValue: null, externalId: false }, - TimeFld: { name: 'TimeFld', type: 'time', size: 10, defaultValue: null, externalId: false }, - StrFld: { name: 'StrFld', type: 'string', size: 100, defaultValue: null, externalId: false }, + IntFld: { + name: 'IntFld', type: 'int', size: 4, defaultValue: null, externalId: false, + }, + RefFld: { + name: 'RefFld', type: 'reference', size: 18, defaultValue: null, externalId: false, + }, + TextFld: { + name: 'TextFld', type: 'textarea', size: 1000, defaultValue: null, externalId: false, + }, + TimeFld: { + name: 'TimeFld', type: 'time', size: 10, defaultValue: null, externalId: false, + }, + StrFld: { + name: 'StrFld', type: 'string', size: 100, defaultValue: null, externalId: false, + }, }, }); @@ -1097,8 +1117,12 @@ test('Test buildTable does NOT create index when index preferences are disabled' const colMock = makeColumnMock(); sfcalls.__set__('proposedSchema', { Account: { - ExtId: { name: 'ExtId', type: 'string', size: 36, defaultValue: null, externalId: true }, - AccountId: { name: 'AccountId', type: 'reference', size: 18, defaultValue: null, externalId: false }, + ExtId: { + name: 'ExtId', type: 'string', size: 36, defaultValue: null, externalId: true, + }, + AccountId: { + name: 'AccountId', type: 'reference', size: 18, defaultValue: null, externalId: false, + }, StageName: { name: 'StageName', type: 'picklist', size: 40, defaultValue: null, externalId: false, values: ['Open'], isRestricted: true, }, @@ -1138,8 +1162,12 @@ test('Test buildDatabase success path sends response_db_generated', async () => sfcalls.__set__('proposedSchema', { Account: { - Id: { name: 'Id', type: 'id', size: 18, defaultValue: null, externalId: false }, - Name: { name: 'Name', type: 'string', size: 255, defaultValue: null, externalId: false }, + Id: { + name: 'Id', type: 'id', size: 18, defaultValue: null, externalId: false, + }, + Name: { + name: 'Name', type: 'string', size: 255, defaultValue: null, externalId: false, + }, }, }); @@ -1174,8 +1202,12 @@ test('Test buildDatabase ER_TOO_BIG_ROWSIZE retry converts string fields and suc sfcalls.__set__('proposedSchema', { BigTable: { - Field1: { name: 'Field1', type: 'string', size: 255, defaultValue: null, externalId: false }, - Field2: { name: 'Field2', type: 'phone', size: 40, defaultValue: null, externalId: false }, + Field1: { + name: 'Field1', type: 'string', size: 255, defaultValue: null, externalId: false, + }, + Field2: { + name: 'Field2', type: 'phone', size: 40, defaultValue: null, externalId: false, + }, }, }); @@ -1227,7 +1259,9 @@ test('Test buildDatabase ER_TOO_MANY_KEYS marks table success and sends response sfcalls.__set__('proposedSchema', { WideTable: { - Id: { name: 'Id', type: 'id', size: 18, defaultValue: null, externalId: false }, + Id: { + name: 'Id', type: 'id', size: 18, defaultValue: null, externalId: false, + }, }, }); @@ -1265,7 +1299,9 @@ test('Test buildDatabase connection failure sends response_db_generated with sta sfcalls.__set__('proposedSchema', { Account: { - Id: { name: 'Id', type: 'id', size: 18, defaultValue: null, externalId: false }, + Id: { + name: 'Id', type: 'id', size: 18, defaultValue: null, externalId: false, + }, }, }); From f72a7c6fca24e5d9b568f4f9f1486e7287c67c86 Mon Sep 17 00:00:00 2001 From: Aaron Crosman Date: Sat, 14 Mar 2026 21:51:58 -0400 Subject: [PATCH 16/16] Adding lint fix to instructions --- .github/copilot-instructions.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0fca66c..4f20f7c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -26,6 +26,12 @@ Follow the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) npm run lint # eslint src app --ignore-path .gitignore ``` +To auto fix linting errors, run: + +```sh +npm run lint:fix #eslint src app --ignore-path .gitignore --fix` +``` + ## Testing Use VS Code's built-in test runner (Jest integration) to run tests. Test files live in `src/tests/` and `app/tests/`.