From a2e1f0a2eda27c98e81d265f028f7314895be581 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Mon, 6 Nov 2023 18:20:36 -0800 Subject: [PATCH 01/21] First pass. --- src/commands/wizard/lwcGenerationCommand.ts | 63 +++++++++++++++++++++ src/utils/uemParser.ts | 34 +++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/utils/uemParser.ts diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 2f109b1..6e5ebe8 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -1,6 +1,11 @@ import { Uri, l10n } from 'vscode'; import { InstructionsWebviewProvider } from '../../webviews/instructions'; +import { InstructionsWebviewProvider } from '../../webviews/instructions'; +import { TemplateChooserCommand } from './templateChooserCommand'; +import { access } from 'fs/promises'; +import { UEMParser } from '../../utils/uemParser'; import * as fs from 'fs'; +import * as path from 'path'; export type QuickActionStatus = { view: boolean; @@ -21,6 +26,54 @@ export class LwcGenerationCommand { this.extensionUri = extensionUri; } + static readFileAsJsonObject(filePath: string, callback: (err: Error | null, data: any) => void): void { + fs.readFile(filePath, 'utf8', (err, data) => { + if (err) { + callback(err, null); + } else { + try { + const jsonObject = JSON.parse(data); + callback(null, jsonObject); + } catch (parseError: any) { + callback(parseError, null); + } + } + }); + } + + static async getCreateLwcPageSobjects(): Promise> { + return new Promise>(async (resolve, reject) => { + let sObjects: Array = []; + let landingPageExists = true; + + const staticResourcesPath = await TemplateChooserCommand.getStaticResourcesDir(); + const landingPageJson = 'landing_page.json'; + const landingPagePath = path.join(staticResourcesPath, landingPageJson); + + try { + await access(landingPagePath); + } catch (err) { + console.warn( + `File '${landingPageJson}' does not exist at '${staticResourcesPath}'.` + ); + landingPageExists = false; + } + + if (landingPageExists) { + this.readFileAsJsonObject(landingPagePath, (error: Error| null, data: any) => { + if (error) { + reject(error); + } else { + const sObjects = UEMParser.findFieldValues(data, 'objectApiName'); + resolve(sObjects); + } + }); + } else { + resolve(sObjects); + } + }); + } + async createSObjectLwcQuickActions() { return new Promise((resolve) => { new InstructionsWebviewProvider( @@ -54,6 +107,16 @@ export class LwcGenerationCommand { callback(quickActionStatus); } } + }, + { + type: 'createLwcPageStatus', + action: async (_panel, _data, callback) => { + if (callback) { + const sObjects = + await LwcGenerationCommand.getCreateLwcPageSobjects(); + callback(sObjects); + } + } } ] ); diff --git a/src/utils/uemParser.ts b/src/utils/uemParser.ts new file mode 100644 index 0000000..b7b7b78 --- /dev/null +++ b/src/utils/uemParser.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +import { l10n } from 'vscode'; +import { Field } from './orgUtils'; + +export class UEMParser { + + public static findFieldValues(json: Object, targetField: String): string[] { + const results:string[] = []; + + function search(json: any) { + for (const key in json) { + if (json.hasOwnProperty(key)) { + if (key === targetField) { + // Only include unique values in the array. + if (!results.includes(json[key])) { + results.push(json[key]); + } + } else if (typeof json[key] === 'object') { + search(json[key]); + } + } + } + } + + search(json); + return results; + } +} From c26a5f8de45c8f5ce3cb6dbb23a77214a591f21f Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Mon, 6 Nov 2023 21:16:28 -0800 Subject: [PATCH 02/21] Wizard page, its command, and util class. --- .../createSObjectLwcQuickActions.html | 148 ++++++++++++------ src/commands/wizard/lwcGenerationCommand.ts | 57 ++++--- src/utils/uemParser.ts | 27 ++-- 3 files changed, 143 insertions(+), 89 deletions(-) diff --git a/resources/instructions/createSObjectLwcQuickActions.html b/resources/instructions/createSObjectLwcQuickActions.html index 69d1c9e..f2d3c1f 100644 --- a/resources/instructions/createSObjectLwcQuickActions.html +++ b/resources/instructions/createSObjectLwcQuickActions.html @@ -35,55 +35,67 @@

Create sObject LWC Quick Actions

-

- The following sObjects are present in your configured landing page: -

- - - - - - - - - - - - - - - - -
sObjectLWC Quick Actions
vieweditcreate
- - - - - - - - \ No newline at end of file + function handlegenerateLwcPageStatusResponse(response) { + if (Object.keys(response).length == 0) { + // If sObjects weren't found hide the table and instead display the error message. + const sobjectDiv = document.getElementById( + 'sobjectDiv' + ); + sobjectDiv.style.display = 'none'; + + const sobjectFoundTableError = document.getElementById( + 'sobjectTableError' + ); + + sobjectFoundTableError.innerText = 'Could not find any sObjects on the landing page'; + } else { + const sobjectFoundTableBody = document.getElementById( + 'sobjectFound' + ); + + const keys = Object.keys(response); + for (const key in keys) { + var newRow = sobjectFoundTableBody.insertRow(); + + var newCell = newRow.insertCell(); + var newText = document.createTextNode(response[key]); + newCell.appendChild(newText); + + // TODO: Update the status after determining if we need a LWC for a sObject. + newCell = newRow.insertCell(); + newText = document.createTextNode('✅'); + newCell.appendChild(newText); + } + } + } + + + + diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 6e5ebe8..5477012 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -1,6 +1,5 @@ import { Uri, l10n } from 'vscode'; import { InstructionsWebviewProvider } from '../../webviews/instructions'; -import { InstructionsWebviewProvider } from '../../webviews/instructions'; import { TemplateChooserCommand } from './templateChooserCommand'; import { access } from 'fs/promises'; import { UEMParser } from '../../utils/uemParser'; @@ -26,29 +25,36 @@ export class LwcGenerationCommand { this.extensionUri = extensionUri; } - static readFileAsJsonObject(filePath: string, callback: (err: Error | null, data: any) => void): void { - fs.readFile(filePath, 'utf8', (err, data) => { - if (err) { - callback(err, null); - } else { - try { - const jsonObject = JSON.parse(data); - callback(null, jsonObject); - } catch (parseError: any) { - callback(parseError, null); + static readFileAsJsonObject( + filePath: string, + callback: (error: Error | null, data: any) => void + ): void { + fs.readFile(filePath, 'utf8', (error, data) => { + if (error) { + callback(error, null); + } else { + try { + const jsonObject = JSON.parse(data); + callback(null, jsonObject); + } catch (parseError: any) { + callback(parseError, null); + } } - } }); } static async getCreateLwcPageSobjects(): Promise> { - return new Promise>(async (resolve, reject) => { + return new Promise>(async (resolve) => { let sObjects: Array = []; let landingPageExists = true; - const staticResourcesPath = await TemplateChooserCommand.getStaticResourcesDir(); + const staticResourcesPath = + await TemplateChooserCommand.getStaticResourcesDir(); const landingPageJson = 'landing_page.json'; - const landingPagePath = path.join(staticResourcesPath, landingPageJson); + const landingPagePath = path.join( + staticResourcesPath, + landingPageJson + ); try { await access(landingPagePath); @@ -60,14 +66,20 @@ export class LwcGenerationCommand { } if (landingPageExists) { - this.readFileAsJsonObject(landingPagePath, (error: Error| null, data: any) => { - if (error) { - reject(error); - } else { - const sObjects = UEMParser.findFieldValues(data, 'objectApiName'); + this.readFileAsJsonObject( + landingPagePath, + (error: Error | null, data: any) => { + if (error) { + console.warn(`Error reading ${landingPageJson}`); + } else { + sObjects = UEMParser.findFieldValues( + data, + 'objectApiName' + ); + } resolve(sObjects); } - }); + ); } else { resolve(sObjects); } @@ -112,8 +124,7 @@ export class LwcGenerationCommand { type: 'createLwcPageStatus', action: async (_panel, _data, callback) => { if (callback) { - const sObjects = - await LwcGenerationCommand.getCreateLwcPageSobjects(); + const sObjects = await LwcGenerationCommand.getCreateLwcPageSobjects(); callback(sObjects); } } diff --git a/src/utils/uemParser.ts b/src/utils/uemParser.ts index b7b7b78..c82e1ce 100644 --- a/src/utils/uemParser.ts +++ b/src/utils/uemParser.ts @@ -9,25 +9,24 @@ import { l10n } from 'vscode'; import { Field } from './orgUtils'; export class UEMParser { - public static findFieldValues(json: Object, targetField: String): string[] { - const results:string[] = []; - + const results: string[] = []; + function search(json: any) { - for (const key in json) { - if (json.hasOwnProperty(key)) { - if (key === targetField) { - // Only include unique values in the array. - if (!results.includes(json[key])) { - results.push(json[key]); + for (const key in json) { + if (json.hasOwnProperty(key)) { + if (key === targetField) { + // Only include unique values in the array. + if (!results.includes(json[key])) { + results.push(json[key]); + } + } else if (typeof json[key] === 'object') { + search(json[key]); + } } - } else if (typeof json[key] === 'object') { - search(json[key]); - } } - } } - + search(json); return results; } From 8a87ff04936e019cd9bd916fd1df3e34d73baf92 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Mon, 6 Nov 2023 21:31:32 -0800 Subject: [PATCH 03/21] Test 1. --- src/test/suite/utils/uemParser.test.ts | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/test/suite/utils/uemParser.test.ts diff --git a/src/test/suite/utils/uemParser.test.ts b/src/test/suite/utils/uemParser.test.ts new file mode 100644 index 0000000..87235e1 --- /dev/null +++ b/src/test/suite/utils/uemParser.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +import * as assert from 'assert'; +import { UEMParser } from '../../../utils/uemParser'; + +suite('UEM Parser Test Suite', () => { + test('Empty object returns empty array', async () => { + const sObjects = UEMParser.findFieldValues({}, 'objectApiName'); + assert.equal(sObjects.length, 0); + }); + + test('Object with a target field returns array size of one', async () => { + const landingPage = { + objectApiName: 'foo' + }; + + const sObjects = UEMParser.findFieldValues(landingPage, 'objectApiName'); + assert.equal(sObjects.length, 1); + assert.equal(sObjects[0], 'foo'); + }); + + test('Nested object returns all values of the target field', async () => { + const landingPage = { + objectApiName: 'foo', + nested: { + objectApiName: 'bar' + } + }; + + const sObjects = UEMParser.findFieldValues(landingPage, 'objectApiName'); + assert.equal(sObjects.length, 2); + assert.equal(sObjects[0], 'foo'); + assert.equal(sObjects[1], 'bar'); + }); + + test('Duplicat field values are omitted', async () => { + const landingPage = { + objectApiName: 'foo', + nested: { + objectApiName: 'bar', + anotherNested: { + objectApiName: 'bar' + } + } + }; + + const sObjects = UEMParser.findFieldValues(landingPage, 'objectApiName'); + assert.equal(sObjects.length, 2); + assert.equal(sObjects[0], 'foo'); + assert.equal(sObjects[1], 'bar'); + }); +}); From fd6c41d181792612c04e6c38252a96aa125385a2 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Mon, 6 Nov 2023 21:49:13 -0800 Subject: [PATCH 04/21] Enable LWC generation. --- src/commands/wizard/lwcGenerationCommand.ts | 3 ++- src/commands/wizard/onboardingWizard.ts | 4 ++++ src/test/suite/utils/uemParser.test.ts | 15 ++++++++++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 5477012..85cfd11 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -124,7 +124,8 @@ export class LwcGenerationCommand { type: 'createLwcPageStatus', action: async (_panel, _data, callback) => { if (callback) { - const sObjects = await LwcGenerationCommand.getCreateLwcPageSobjects(); + const sObjects = + await LwcGenerationCommand.getCreateLwcPageSobjects(); callback(sObjects); } } diff --git a/src/commands/wizard/onboardingWizard.ts b/src/commands/wizard/onboardingWizard.ts index f128aeb..6840939 100644 --- a/src/commands/wizard/onboardingWizard.ts +++ b/src/commands/wizard/onboardingWizard.ts @@ -33,6 +33,10 @@ async function runPostProjectConfigurationSteps( await AuthorizeCommand.authorizeToOrg(); await DeployToOrgCommand.deployToOrg(); + await new LwcGenerationCommand( + extensionUri + ).createSObjectLwcQuickActions(); + await InstructionsWebviewProvider.showDismissableInstructions( extensionUri, vscode.l10n.t('View in the Salesforce Mobile App'), diff --git a/src/test/suite/utils/uemParser.test.ts b/src/test/suite/utils/uemParser.test.ts index 87235e1..199a0eb 100644 --- a/src/test/suite/utils/uemParser.test.ts +++ b/src/test/suite/utils/uemParser.test.ts @@ -19,7 +19,10 @@ suite('UEM Parser Test Suite', () => { objectApiName: 'foo' }; - const sObjects = UEMParser.findFieldValues(landingPage, 'objectApiName'); + const sObjects = UEMParser.findFieldValues( + landingPage, + 'objectApiName' + ); assert.equal(sObjects.length, 1); assert.equal(sObjects[0], 'foo'); }); @@ -32,7 +35,10 @@ suite('UEM Parser Test Suite', () => { } }; - const sObjects = UEMParser.findFieldValues(landingPage, 'objectApiName'); + const sObjects = UEMParser.findFieldValues( + landingPage, + 'objectApiName' + ); assert.equal(sObjects.length, 2); assert.equal(sObjects[0], 'foo'); assert.equal(sObjects[1], 'bar'); @@ -49,7 +55,10 @@ suite('UEM Parser Test Suite', () => { } }; - const sObjects = UEMParser.findFieldValues(landingPage, 'objectApiName'); + const sObjects = UEMParser.findFieldValues( + landingPage, + 'objectApiName' + ); assert.equal(sObjects.length, 2); assert.equal(sObjects[0], 'foo'); assert.equal(sObjects[1], 'bar'); From 415176714dfcdb43c7a96be98753d6577126f82d Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Tue, 7 Nov 2023 14:17:26 -0800 Subject: [PATCH 05/21] Use status object to report error back. --- .../createSObjectLwcQuickActions.html | 44 +++++++++---------- src/commands/wizard/lwcGenerationCommand.ts | 30 ++++++++----- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/resources/instructions/createSObjectLwcQuickActions.html b/resources/instructions/createSObjectLwcQuickActions.html index f2d3c1f..5030104 100644 --- a/resources/instructions/createSObjectLwcQuickActions.html +++ b/resources/instructions/createSObjectLwcQuickActions.html @@ -97,27 +97,26 @@

Create sObject LWC Quick Actions

}); - function handleQuickActionStatusResponse(response) { - const table = document.getElementById('quickActionStatusTable'); - for (const sobject in response.sobjects) { - const quickActions = response.sobjects[sobject]; - - const sobjectRow = table.insertRow(); - const name = sobjectRow.insertCell(0); - const view = sobjectRow.insertCell(1); - const edit = sobjectRow.insertCell(2); - const create = sobjectRow.insertCell(3); - - name.innerText = sobject; - view.innerHTML = quickActions.view == true ? "✅" : "❌"; - edit.innerHTML = quickActions.edit == true ? "✅" : "❌"; - create.innerHTML = quickActions.create == true ? "✅" : "❌"; + function handleQuickActionStatusResponse(response) { + const table = document.getElementById('quickActionStatusTable'); + for (const sobject in response.sobjects) { + const quickActions = response.sobjects[sobject]; + + const sobjectRow = table.insertRow(); + const name = sobjectRow.insertCell(0); + const view = sobjectRow.insertCell(1); + const edit = sobjectRow.insertCell(2); + const create = sobjectRow.insertCell(3); + + name.innerText = sobject; + view.innerHTML = quickActions.view == true ? "✅" : "❌"; + edit.innerHTML = quickActions.edit == true ? "✅" : "❌"; + create.innerHTML = quickActions.create == true ? "✅" : "❌"; + } } - } - - function handlegenerateLwcPageStatusResponse(response) { - if (Object.keys(response).length == 0) { - // If sObjects weren't found hide the table and instead display the error message. + function handlegenerateLwcPageStatusResponse(response) { + if (response.error) { + // If sObjects weren't found then hide the table and instead display the error message. const sobjectDiv = document.getElementById( 'sobjectDiv' ); @@ -133,12 +132,11 @@

Create sObject LWC Quick Actions

'sobjectFound' ); - const keys = Object.keys(response); - for (const key in keys) { + for (const sobject of response.sobjects) { var newRow = sobjectFoundTableBody.insertRow(); var newCell = newRow.insertCell(); - var newText = document.createTextNode(response[key]); + var newText = document.createTextNode(sobject); newCell.appendChild(newText); // TODO: Update the status after determining if we need a LWC for a sObject. diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 85cfd11..cb40a05 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -18,6 +18,11 @@ export type SObjectQuickActionStatus = { }; }; +export type LwcGenerationCommandStatus = { + error?: string; + sobjects: string[]; +}; + export class LwcGenerationCommand { extensionUri: Uri; @@ -43,9 +48,8 @@ export class LwcGenerationCommand { }); } - static async getCreateLwcPageSobjects(): Promise> { - return new Promise>(async (resolve) => { - let sObjects: Array = []; + static async getLwcGenerationPageStatus(): Promise { + return new Promise(async (resolve) => { let landingPageExists = true; const staticResourcesPath = @@ -56,6 +60,10 @@ export class LwcGenerationCommand { landingPageJson ); + const lwcGenerationCommandStatus: LwcGenerationCommandStatus = { + sobjects: [] + }; + try { await access(landingPagePath); } catch (err) { @@ -63,6 +71,7 @@ export class LwcGenerationCommand { `File '${landingPageJson}' does not exist at '${staticResourcesPath}'.` ); landingPageExists = false; + lwcGenerationCommandStatus.error = (err as Error).message; } if (landingPageExists) { @@ -71,17 +80,18 @@ export class LwcGenerationCommand { (error: Error | null, data: any) => { if (error) { console.warn(`Error reading ${landingPageJson}`); + lwcGenerationCommandStatus.error = (error as Error).message; } else { - sObjects = UEMParser.findFieldValues( + lwcGenerationCommandStatus.sobjects = UEMParser.findFieldValues( data, 'objectApiName' ); } - resolve(sObjects); + resolve(lwcGenerationCommandStatus); } ); } else { - resolve(sObjects); + resolve(lwcGenerationCommandStatus); } }); } @@ -121,12 +131,12 @@ export class LwcGenerationCommand { } }, { - type: 'createLwcPageStatus', + type: 'generateLwcPageStatus', action: async (_panel, _data, callback) => { if (callback) { - const sObjects = - await LwcGenerationCommand.getCreateLwcPageSobjects(); - callback(sObjects); + const lwcGenerationPageStatus = + await LwcGenerationCommand.getLwcGenerationPageStatus(); + callback(lwcGenerationPageStatus); } } } From f9511be697c968b0141e8a700a2f5b590903a8b2 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Tue, 7 Nov 2023 19:09:55 -0800 Subject: [PATCH 06/21] Unit tests for the command. --- .../createSObjectLwcQuickActions.html | 2 +- src/commands/wizard/lwcGenerationCommand.ts | 34 ++++--- .../wizard/lwcGenerationCommand.test.ts | 96 +++++++++++++++++++ 3 files changed, 113 insertions(+), 19 deletions(-) diff --git a/resources/instructions/createSObjectLwcQuickActions.html b/resources/instructions/createSObjectLwcQuickActions.html index 5030104..8f45864 100644 --- a/resources/instructions/createSObjectLwcQuickActions.html +++ b/resources/instructions/createSObjectLwcQuickActions.html @@ -126,7 +126,7 @@

Create sObject LWC Quick Actions

'sobjectTableError' ); - sobjectFoundTableError.innerText = 'Could not find any sObjects on the landing page'; + sobjectFoundTableError.innerText = `Could not find any sObjects on the landing page: ${response.error}`; } else { const sobjectFoundTableBody = document.getElementById( 'sobjectFound' diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index cb40a05..e73dab5 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -34,18 +34,13 @@ export class LwcGenerationCommand { filePath: string, callback: (error: Error | null, data: any) => void ): void { - fs.readFile(filePath, 'utf8', (error, data) => { - if (error) { - callback(error, null); - } else { - try { - const jsonObject = JSON.parse(data); - callback(null, jsonObject); - } catch (parseError: any) { - callback(parseError, null); - } - } - }); + try { + const data = fs.readFileSync(filePath, { encoding: 'utf-8' }); + const jsonObject = JSON.parse(data); + callback(null, jsonObject); + } catch (error) { + callback(error as Error, null); + } } static async getLwcGenerationPageStatus(): Promise { @@ -80,14 +75,17 @@ export class LwcGenerationCommand { (error: Error | null, data: any) => { if (error) { console.warn(`Error reading ${landingPageJson}`); - lwcGenerationCommandStatus.error = (error as Error).message; + lwcGenerationCommandStatus.error = ( + error as Error + ).message; } else { - lwcGenerationCommandStatus.sobjects = UEMParser.findFieldValues( - data, - 'objectApiName' - ); + lwcGenerationCommandStatus.sobjects = + UEMParser.findFieldValues( + data, + 'objectApiName' + ); } - resolve(lwcGenerationCommandStatus); + resolve(lwcGenerationCommandStatus); } ); } else { diff --git a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts index d9707cb..3d19ad1 100644 --- a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts +++ b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts @@ -87,4 +87,100 @@ suite('LWC Generation Command Test Suite', () => { 'sobject2.create should NOT exist' ); }); + + const validJsonFile = 'valid.json'; + const invalidJsonFile = 'invalid.json'; + const jsonContents = '{"name": "John", "age": 30}'; + const invalidJsonContents = 'invalid_json_here'; + + test('should read a valid JSON file and parse it', (done) => { + fs.writeFileSync(validJsonFile, jsonContents, 'utf8'); + + LwcGenerationCommand.readFileAsJsonObject( + validJsonFile, + (err, data) => { + assert.equal(err, null); + assert.deepEqual(data, { name: 'John', age: 30 }); + + fs.unlinkSync(validJsonFile); + + done(); + } + ); + }); + + test('should handle invalid JSON and return an error', (done) => { + fs.writeFileSync(invalidJsonFile, invalidJsonContents, 'utf8'); + + LwcGenerationCommand.readFileAsJsonObject( + invalidJsonFile, + (err, data) => { + assert.equal( + err?.message, + `Unexpected token 'i', "invalid_json_here" is not valid JSON` + ); + assert.equal(data, null); + + fs.unlinkSync(invalidJsonFile); + + done(); + } + ); + }); + + test('should handle file not found and return an error', (done) => { + const nonExistentFile = 'non_existent.json'; + LwcGenerationCommand.readFileAsJsonObject( + nonExistentFile, + (err, data) => { + assert.equal(data, null); + assert.equal( + err?.message, + "ENOENT: no such file or directory, open 'non_existent.json'" + ); + + done(); + } + ); + }); + + test('should return error status for landing page with invalid json', async () => { + const getWorkspaceDirStub = sinon.stub( + TemplateChooserCommand, + 'getStaticResourcesDir' + ); + getWorkspaceDirStub.returns(Promise.resolve('.')); + const fsAccess = sinon.stub(fs, 'access'); + fsAccess.returns(); + const invalidJsonFile = 'landing_page.json'; + fs.writeFileSync(invalidJsonFile, invalidJsonContents, 'utf8'); + + const status = await LwcGenerationCommand.getLwcGenerationPageStatus(); + + assert.ok(status.error && status.error.length > 0); + + fs.unlinkSync(invalidJsonFile); + }); + + test('should return 2 sObjects', async () => { + const getWorkspaceDirStub = sinon.stub( + TemplateChooserCommand, + 'getStaticResourcesDir' + ); + getWorkspaceDirStub.returns(Promise.resolve('.')); + const fsAccess = sinon.stub(fs, 'access'); + fsAccess.returns(); + const validJsonFile = 'landing_page.json'; + const jsonContents = + '{"objectApiName": "Account", "nested": {"objectApiName": "Contact"}}'; + fs.writeFileSync(validJsonFile, jsonContents, 'utf8'); + + const status = await LwcGenerationCommand.getLwcGenerationPageStatus(); + + assert.equal(status.sobjects.length, 2); + assert.equal(status.sobjects[0], 'Account'); + assert.equal(status.sobjects[1], 'Contact'); + + fs.unlinkSync(validJsonFile); + }); }); From 0f77ed7b49d0c227c6acf16411b9747ad47fe43f Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Wed, 8 Nov 2023 11:31:01 -0800 Subject: [PATCH 07/21] Case. --- resources/instructions/createSObjectLwcQuickActions.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/resources/instructions/createSObjectLwcQuickActions.html b/resources/instructions/createSObjectLwcQuickActions.html index 8f45864..3ece845 100644 --- a/resources/instructions/createSObjectLwcQuickActions.html +++ b/resources/instructions/createSObjectLwcQuickActions.html @@ -92,7 +92,7 @@

Create sObject LWC Quick Actions

webviewMessaging.sendMessageRequest( 'generateLwcPageStatus', {}, - handlegenerateLwcPageStatusResponse + handleGenerateLwcPageStatusResponse ); }); @@ -114,8 +114,9 @@

Create sObject LWC Quick Actions

create.innerHTML = quickActions.create == true ? "✅" : "❌"; } } - function handlegenerateLwcPageStatusResponse(response) { - if (response.error) { + + function handleGenerateLwcPageStatusResponse(response) { + if (response.error) { // If sObjects weren't found then hide the table and instead display the error message. const sobjectDiv = document.getElementById( 'sobjectDiv' From 5626346aa8f6cfd471eaa590d646c71436b1ac5d Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Wed, 8 Nov 2023 15:25:29 -0800 Subject: [PATCH 08/21] Mash QA detection and sObject parse. Resolve merge conflict. --- .../createSObjectLwcQuickActions.html | 47 +--------------- src/commands/wizard/lwcGenerationCommand.ts | 54 +++++++------------ .../wizard/lwcGenerationCommand.test.ts | 18 +++---- 3 files changed, 30 insertions(+), 89 deletions(-) diff --git a/resources/instructions/createSObjectLwcQuickActions.html b/resources/instructions/createSObjectLwcQuickActions.html index 3ece845..973f646 100644 --- a/resources/instructions/createSObjectLwcQuickActions.html +++ b/resources/instructions/createSObjectLwcQuickActions.html @@ -35,13 +35,11 @@

Create sObject LWC Quick Actions

-

-

The following sObjects are present in your configured landing page:

- +
@@ -53,7 +51,7 @@

Create sObject LWC Quick Actions

- +
sObject create
@@ -87,14 +85,6 @@

Create sObject LWC Quick Actions

{}, handleQuickActionStatusResponse ); - - // Are there any sObjects on the landing page - webviewMessaging.sendMessageRequest( - 'generateLwcPageStatus', - {}, - handleGenerateLwcPageStatusResponse - ); - }); function handleQuickActionStatusResponse(response) { @@ -114,39 +104,6 @@

Create sObject LWC Quick Actions

create.innerHTML = quickActions.create == true ? "✅" : "❌"; } } - - function handleGenerateLwcPageStatusResponse(response) { - if (response.error) { - // If sObjects weren't found then hide the table and instead display the error message. - const sobjectDiv = document.getElementById( - 'sobjectDiv' - ); - sobjectDiv.style.display = 'none'; - - const sobjectFoundTableError = document.getElementById( - 'sobjectTableError' - ); - - sobjectFoundTableError.innerText = `Could not find any sObjects on the landing page: ${response.error}`; - } else { - const sobjectFoundTableBody = document.getElementById( - 'sobjectFound' - ); - - for (const sobject of response.sobjects) { - var newRow = sobjectFoundTableBody.insertRow(); - - var newCell = newRow.insertCell(); - var newText = document.createTextNode(sobject); - newCell.appendChild(newText); - - // TODO: Update the status after determining if we need a LWC for a sObject. - newCell = newRow.insertCell(); - newText = document.createTextNode('✅'); - newCell.appendChild(newText); - } - } - } diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index e73dab5..48b5d92 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -13,12 +13,13 @@ export type QuickActionStatus = { }; export type SObjectQuickActionStatus = { + error?: string; sobjects: { [name: string]: QuickActionStatus; }; }; -export type LwcGenerationCommandStatus = { +export type GetSObjectsStatus = { error?: string; sobjects: string[]; }; @@ -43,8 +44,8 @@ export class LwcGenerationCommand { } } - static async getLwcGenerationPageStatus(): Promise { - return new Promise(async (resolve) => { + static async getSObjectsFromLandingPage(): Promise { + return new Promise(async (resolve) => { let landingPageExists = true; const staticResourcesPath = @@ -55,7 +56,7 @@ export class LwcGenerationCommand { landingPageJson ); - const lwcGenerationCommandStatus: LwcGenerationCommandStatus = { + const getSObjectsStatus: GetSObjectsStatus = { sobjects: [] }; @@ -66,7 +67,7 @@ export class LwcGenerationCommand { `File '${landingPageJson}' does not exist at '${staticResourcesPath}'.` ); landingPageExists = false; - lwcGenerationCommandStatus.error = (err as Error).message; + getSObjectsStatus.error = (err as Error).message; } if (landingPageExists) { @@ -75,21 +76,19 @@ export class LwcGenerationCommand { (error: Error | null, data: any) => { if (error) { console.warn(`Error reading ${landingPageJson}`); - lwcGenerationCommandStatus.error = ( - error as Error - ).message; + getSObjectsStatus.error = (error as Error).message; } else { - lwcGenerationCommandStatus.sobjects = + getSObjectsStatus.sobjects = UEMParser.findFieldValues( data, 'objectApiName' ); } - resolve(lwcGenerationCommandStatus); + resolve(getSObjectsStatus); } ); } else { - resolve(lwcGenerationCommandStatus); + resolve(getSObjectsStatus); } }); } @@ -112,44 +111,29 @@ export class LwcGenerationCommand { { type: 'getQuickActionStatus', action: async (_panel, _data, callback) => { - // TODO: Hook this up to function that parses landing_page.json. - const sobjects = [ - 'Account', - 'Contact', - 'Opportunity', - 'SomeOther' - ]; if (callback) { const quickActionStatus = - await LwcGenerationCommand.checkForExistingQuickActions( - sobjects - ); + await LwcGenerationCommand.checkForExistingQuickActions(); callback(quickActionStatus); } } - }, - { - type: 'generateLwcPageStatus', - action: async (_panel, _data, callback) => { - if (callback) { - const lwcGenerationPageStatus = - await LwcGenerationCommand.getLwcGenerationPageStatus(); - callback(lwcGenerationPageStatus); - } - } } ] ); }); } - static async checkForExistingQuickActions( - sobjects: string[] - ): Promise { + static async checkForExistingQuickActions(): Promise { return new Promise(async (resolve) => { const results: SObjectQuickActionStatus = { sobjects: {} }; - sobjects.forEach((sobject) => { + const sObjectsStatus = await this.getSObjectsFromLandingPage(); + if (sObjectsStatus.error) { + results.error = sObjectsStatus.error; + return resolve(results); + } + + sObjectsStatus.sobjects.forEach((sobject) => { const quickActionStatus: QuickActionStatus = { view: false, edit: false, diff --git a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts index 3d19ad1..0c0165c 100644 --- a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts +++ b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts @@ -11,9 +11,9 @@ import * as fs from 'fs'; import { afterEach, beforeEach } from 'mocha'; import { LwcGenerationCommand, - SObjectQuickActionStatus, - QuickActionStatus + SObjectQuickActionStatus } from '../../../../commands/wizard/lwcGenerationCommand'; +import { TemplateChooserCommand } from '../../../../commands/wizard/templateChooserCommand'; suite('LWC Generation Command Test Suite', () => { beforeEach(function () {}); @@ -49,11 +49,11 @@ suite('LWC Generation Command Test Suite', () => { .withArgs(`${baseDir}/sobject2.create.quickAction-meta.xml`) .throws('error'); - const result: SObjectQuickActionStatus = - await LwcGenerationCommand.checkForExistingQuickActions([ - 'sobject1', - 'sobject2' - ]); + const getSObjectsStub = sinon.stub(LwcGenerationCommand, "getSObjectsFromLandingPage"); + getSObjectsStub.returns(Promise.resolve({sobjects: ["sobject1", "sobject2"]})); + + const result: SObjectQuickActionStatus = await LwcGenerationCommand + .checkForExistingQuickActions(); assert.equal( result.sobjects['sobject1'].view, @@ -155,7 +155,7 @@ suite('LWC Generation Command Test Suite', () => { const invalidJsonFile = 'landing_page.json'; fs.writeFileSync(invalidJsonFile, invalidJsonContents, 'utf8'); - const status = await LwcGenerationCommand.getLwcGenerationPageStatus(); + const status = await LwcGenerationCommand.getSObjectsFromLandingPage(); assert.ok(status.error && status.error.length > 0); @@ -175,7 +175,7 @@ suite('LWC Generation Command Test Suite', () => { '{"objectApiName": "Account", "nested": {"objectApiName": "Contact"}}'; fs.writeFileSync(validJsonFile, jsonContents, 'utf8'); - const status = await LwcGenerationCommand.getLwcGenerationPageStatus(); + const status = await LwcGenerationCommand.getSObjectsFromLandingPage(); assert.equal(status.sobjects.length, 2); assert.equal(status.sobjects[0], 'Account'); From 9e6d3c5045fe8faceade0815cc5bddef5543da70 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Wed, 8 Nov 2023 22:01:20 -0800 Subject: [PATCH 09/21] Use CommUtils. Remove nested function for recursion. Parse for specific values. --- package-lock.json | 10 +++ package.json | 1 + src/commands/wizard/lwcGenerationCommand.ts | 36 ++--------- src/commands/wizard/onboardingWizard.ts | 5 -- src/utils/uemParser.ts | 68 ++++++++++++++++----- 5 files changed, 68 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd8bc00..fbcb398 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/cli-progress": "^3.11.5", "@types/glob": "^8.1.0", "@types/inquirer": "^9.0.6", "@types/mocha": "^10.0.3", @@ -1528,6 +1529,15 @@ "node": ">= 6" } }, + "node_modules/@types/cli-progress": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz", + "integrity": "sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", diff --git a/package.json b/package.json index 753b3f1..06106d9 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/cli-progress": "^3.11.5", "@types/glob": "^8.1.0", "@types/inquirer": "^9.0.6", "@types/mocha": "^10.0.3", diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 48b5d92..944db28 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -3,6 +3,7 @@ import { InstructionsWebviewProvider } from '../../webviews/instructions'; import { TemplateChooserCommand } from './templateChooserCommand'; import { access } from 'fs/promises'; import { UEMParser } from '../../utils/uemParser'; +import { CommonUtils } from '@salesforce/lwc-dev-mobile-core'; import * as fs from 'fs'; import * as path from 'path'; @@ -31,19 +32,6 @@ export class LwcGenerationCommand { this.extensionUri = extensionUri; } - static readFileAsJsonObject( - filePath: string, - callback: (error: Error | null, data: any) => void - ): void { - try { - const data = fs.readFileSync(filePath, { encoding: 'utf-8' }); - const jsonObject = JSON.parse(data); - callback(null, jsonObject); - } catch (error) { - callback(error as Error, null); - } - } - static async getSObjectsFromLandingPage(): Promise { return new Promise(async (resolve) => { let landingPageExists = true; @@ -71,25 +59,11 @@ export class LwcGenerationCommand { } if (landingPageExists) { - this.readFileAsJsonObject( - landingPagePath, - (error: Error | null, data: any) => { - if (error) { - console.warn(`Error reading ${landingPageJson}`); - getSObjectsStatus.error = (error as Error).message; - } else { - getSObjectsStatus.sobjects = - UEMParser.findFieldValues( - data, - 'objectApiName' - ); - } - resolve(getSObjectsStatus); - } - ); - } else { - resolve(getSObjectsStatus); + const uem = CommonUtils.loadJsonFromFile(landingPagePath); + getSObjectsStatus.sobjects = UEMParser.findSObjects(uem); } + + resolve(getSObjectsStatus); }); } diff --git a/src/commands/wizard/onboardingWizard.ts b/src/commands/wizard/onboardingWizard.ts index 6840939..f27f258 100644 --- a/src/commands/wizard/onboardingWizard.ts +++ b/src/commands/wizard/onboardingWizard.ts @@ -12,7 +12,6 @@ import { DeployToOrgCommand } from './deployToOrgCommand'; import { ConfigureProjectCommand } from './configureProjectCommand'; import { AuthorizeCommand } from './authorizeCommand'; import { InstructionsWebviewProvider } from '../../webviews/instructions'; -import { LwcGenerationCommand } from './lwcGenerationCommand'; const wizardCommand = 'salesforcedx-vscode-offline-app.onboardingWizard'; const onboardingWizardStateKey = @@ -33,10 +32,6 @@ async function runPostProjectConfigurationSteps( await AuthorizeCommand.authorizeToOrg(); await DeployToOrgCommand.deployToOrg(); - await new LwcGenerationCommand( - extensionUri - ).createSObjectLwcQuickActions(); - await InstructionsWebviewProvider.showDismissableInstructions( extensionUri, vscode.l10n.t('View in the Salesforce Mobile App'), diff --git a/src/utils/uemParser.ts b/src/utils/uemParser.ts index c82e1ce..5eb0f4d 100644 --- a/src/utils/uemParser.ts +++ b/src/utils/uemParser.ts @@ -5,29 +5,65 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { l10n } from 'vscode'; -import { Field } from './orgUtils'; - export class UEMParser { - public static findFieldValues(json: Object, targetField: String): string[] { + public static findSObjects(json: Object): Array { + const sObjects = UEMParser.findObjectsWithValues(json, [ + 'mcf/list', + 'mcf/timedList', + 'mcf/genericLists' + ]); const results: string[] = []; - function search(json: any) { - for (const key in json) { - if (json.hasOwnProperty(key)) { - if (key === targetField) { - // Only include unique values in the array. - if (!results.includes(json[key])) { - results.push(json[key]); - } - } else if (typeof json[key] === 'object') { - search(json[key]); - } + sObjects.forEach((obj) => { + let properties = obj['properties' as keyof Object]; + let objectApiName = properties[ + 'objectApiName' as keyof Object + ] as unknown as string; + + // Only include unique values in the array. + if (!results.includes(objectApiName)) { + results.push(objectApiName); + } + }); + + return results; + } + + static findObjectsWithValues( + json: Object, + valuesToMatch: string[] + ): Array { + const results: Array = []; + + if (typeof json === 'object') { + if (Array.isArray(json)) { + for (const item of json) { + results.push( + ...UEMParser.findObjectsWithValues(item, valuesToMatch) + ); + } + } else { + const values = Object.values(json); + + const matched = valuesToMatch.some((value) => + values.includes(value) + ); + + if (matched) { + results.push(json); + } + + for (const key in json) { + results.push( + ...UEMParser.findObjectsWithValues( + json[key as keyof Object], + valuesToMatch + ) + ); } } } - search(json); return results; } } From f95b23952ab2823ca48bf02acf3cb3adb7163576 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Wed, 8 Nov 2023 22:28:30 -0800 Subject: [PATCH 10/21] Updated tests. --- src/commands/wizard/lwcGenerationCommand.ts | 10 +-- .../wizard/lwcGenerationCommand.test.ts | 72 +++--------------- src/test/suite/utils/uemParser.test.ts | 76 ++++++++++++++----- src/utils/uemParser.ts | 2 +- 4 files changed, 70 insertions(+), 90 deletions(-) diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 944db28..6fc2fd7 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -34,8 +34,6 @@ export class LwcGenerationCommand { static async getSObjectsFromLandingPage(): Promise { return new Promise(async (resolve) => { - let landingPageExists = true; - const staticResourcesPath = await TemplateChooserCommand.getStaticResourcesDir(); const landingPageJson = 'landing_page.json'; @@ -50,19 +48,15 @@ export class LwcGenerationCommand { try { await access(landingPagePath); + const uem = CommonUtils.loadJsonFromFile(landingPagePath); + getSObjectsStatus.sobjects = UEMParser.findSObjects(uem); } catch (err) { console.warn( `File '${landingPageJson}' does not exist at '${staticResourcesPath}'.` ); - landingPageExists = false; getSObjectsStatus.error = (err as Error).message; } - if (landingPageExists) { - const uem = CommonUtils.loadJsonFromFile(landingPagePath); - getSObjectsStatus.sobjects = UEMParser.findSObjects(uem); - } - resolve(getSObjectsStatus); }); } diff --git a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts index 0c0165c..371cf53 100644 --- a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts +++ b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts @@ -49,11 +49,16 @@ suite('LWC Generation Command Test Suite', () => { .withArgs(`${baseDir}/sobject2.create.quickAction-meta.xml`) .throws('error'); - const getSObjectsStub = sinon.stub(LwcGenerationCommand, "getSObjectsFromLandingPage"); - getSObjectsStub.returns(Promise.resolve({sobjects: ["sobject1", "sobject2"]})); + const getSObjectsStub = sinon.stub( + LwcGenerationCommand, + 'getSObjectsFromLandingPage' + ); + getSObjectsStub.returns( + Promise.resolve({ sobjects: ['sobject1', 'sobject2'] }) + ); - const result: SObjectQuickActionStatus = await LwcGenerationCommand - .checkForExistingQuickActions(); + const result: SObjectQuickActionStatus = + await LwcGenerationCommand.checkForExistingQuickActions(); assert.equal( result.sobjects['sobject1'].view, @@ -88,62 +93,6 @@ suite('LWC Generation Command Test Suite', () => { ); }); - const validJsonFile = 'valid.json'; - const invalidJsonFile = 'invalid.json'; - const jsonContents = '{"name": "John", "age": 30}'; - const invalidJsonContents = 'invalid_json_here'; - - test('should read a valid JSON file and parse it', (done) => { - fs.writeFileSync(validJsonFile, jsonContents, 'utf8'); - - LwcGenerationCommand.readFileAsJsonObject( - validJsonFile, - (err, data) => { - assert.equal(err, null); - assert.deepEqual(data, { name: 'John', age: 30 }); - - fs.unlinkSync(validJsonFile); - - done(); - } - ); - }); - - test('should handle invalid JSON and return an error', (done) => { - fs.writeFileSync(invalidJsonFile, invalidJsonContents, 'utf8'); - - LwcGenerationCommand.readFileAsJsonObject( - invalidJsonFile, - (err, data) => { - assert.equal( - err?.message, - `Unexpected token 'i', "invalid_json_here" is not valid JSON` - ); - assert.equal(data, null); - - fs.unlinkSync(invalidJsonFile); - - done(); - } - ); - }); - - test('should handle file not found and return an error', (done) => { - const nonExistentFile = 'non_existent.json'; - LwcGenerationCommand.readFileAsJsonObject( - nonExistentFile, - (err, data) => { - assert.equal(data, null); - assert.equal( - err?.message, - "ENOENT: no such file or directory, open 'non_existent.json'" - ); - - done(); - } - ); - }); - test('should return error status for landing page with invalid json', async () => { const getWorkspaceDirStub = sinon.stub( TemplateChooserCommand, @@ -153,6 +102,7 @@ suite('LWC Generation Command Test Suite', () => { const fsAccess = sinon.stub(fs, 'access'); fsAccess.returns(); const invalidJsonFile = 'landing_page.json'; + const invalidJsonContents = 'invalid_json_here'; fs.writeFileSync(invalidJsonFile, invalidJsonContents, 'utf8'); const status = await LwcGenerationCommand.getSObjectsFromLandingPage(); @@ -172,7 +122,7 @@ suite('LWC Generation Command Test Suite', () => { fsAccess.returns(); const validJsonFile = 'landing_page.json'; const jsonContents = - '{"objectApiName": "Account", "nested": {"objectApiName": "Contact"}}'; + '{ "definition": "mcf/list", "properties": { "objectApiName": "Account" }, "nested": { "definition": "mcf/timedList", "properties": { "objectApiName": "Contact"} } }'; fs.writeFileSync(validJsonFile, jsonContents, 'utf8'); const status = await LwcGenerationCommand.getSObjectsFromLandingPage(); diff --git a/src/test/suite/utils/uemParser.test.ts b/src/test/suite/utils/uemParser.test.ts index 199a0eb..1e35c98 100644 --- a/src/test/suite/utils/uemParser.test.ts +++ b/src/test/suite/utils/uemParser.test.ts @@ -10,57 +10,93 @@ import { UEMParser } from '../../../utils/uemParser'; suite('UEM Parser Test Suite', () => { test('Empty object returns empty array', async () => { - const sObjects = UEMParser.findFieldValues({}, 'objectApiName'); + const sObjects = UEMParser.findSObjects({}); assert.equal(sObjects.length, 0); }); test('Object with a target field returns array size of one', async () => { const landingPage = { - objectApiName: 'foo' + definition: 'mcf/list', + properties: { + objectApiName: 'foo' + } }; - const sObjects = UEMParser.findFieldValues( - landingPage, - 'objectApiName' - ); + const sObjects = UEMParser.findSObjects(landingPage); assert.equal(sObjects.length, 1); assert.equal(sObjects[0], 'foo'); }); test('Nested object returns all values of the target field', async () => { const landingPage = { - objectApiName: 'foo', + definition: 'mcf/list', + properties: { + objectApiName: 'foo' + }, nested: { - objectApiName: 'bar' + definition: 'mcf/list', + properties: { + objectApiName: 'bar' + } } }; - const sObjects = UEMParser.findFieldValues( - landingPage, - 'objectApiName' - ); + const sObjects = UEMParser.findSObjects(landingPage); assert.equal(sObjects.length, 2); assert.equal(sObjects[0], 'foo'); assert.equal(sObjects[1], 'bar'); }); - test('Duplicat field values are omitted', async () => { + test('Duplicate field values are omitted', async () => { const landingPage = { - objectApiName: 'foo', + definition: 'mcf/list', + properties: { + objectApiName: 'foo' + }, nested: { - objectApiName: 'bar', - anotherNested: { + definition: 'mcf/list', + properties: { objectApiName: 'bar' + }, + anotherNested: { + definition: 'mcf/list', + properties: { + objectApiName: 'bar' + } } } }; - const sObjects = UEMParser.findFieldValues( - landingPage, - 'objectApiName' - ); + const sObjects = UEMParser.findSObjects(landingPage); assert.equal(sObjects.length, 2); assert.equal(sObjects[0], 'foo'); assert.equal(sObjects[1], 'bar'); }); + + test('Duplicat field values are omitted', async () => { + const landingPage = { + definition: 'mcf/list', + properties: { + objectApiName: 'plain' + }, + nested: { + definition: 'mcf/timedList', + properties: { + objectApiName: 'timed' + }, + anotherNested: { + definition: 'mcf/genericLists', + properties: { + objectApiName: 'generic' + } + } + } + }; + + const sObjects = UEMParser.findSObjects(landingPage); + assert.equal(sObjects.length, 3); + assert.equal(sObjects[0], 'plain'); + assert.equal(sObjects[1], 'timed'); + assert.equal(sObjects[2], 'generic'); + }); }); diff --git a/src/utils/uemParser.ts b/src/utils/uemParser.ts index 5eb0f4d..0cffa40 100644 --- a/src/utils/uemParser.ts +++ b/src/utils/uemParser.ts @@ -19,7 +19,7 @@ export class UEMParser { let objectApiName = properties[ 'objectApiName' as keyof Object ] as unknown as string; - + // Only include unique values in the array. if (!results.includes(objectApiName)) { results.push(objectApiName); From efb5c3b853ed054d5ae7f663270cf2ef7f69e380 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Wed, 8 Nov 2023 22:59:26 -0800 Subject: [PATCH 11/21] Moving out methods from template chooser command. --- src/commands/wizard/lwcGenerationCommand.ts | 7 +- src/commands/wizard/templateChooserCommand.ts | 36 +------- .../wizard/lwcGenerationCommand.test.ts | 6 +- .../wizard/templateChooserCommand.test.ts | 87 ++----------------- src/test/suite/utils/uiUtils.test.ts | 59 +++++++++++++ src/utils/uiUtils.ts | 50 ++++++++++- 6 files changed, 126 insertions(+), 119 deletions(-) diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 6fc2fd7..3b122a4 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -1,8 +1,8 @@ import { Uri, l10n } from 'vscode'; -import { InstructionsWebviewProvider } from '../../webviews/instructions'; -import { TemplateChooserCommand } from './templateChooserCommand'; import { access } from 'fs/promises'; +import { InstructionsWebviewProvider } from '../../webviews/instructions'; import { UEMParser } from '../../utils/uemParser'; +import { UIUtils } from '../../utils/uiUtils'; import { CommonUtils } from '@salesforce/lwc-dev-mobile-core'; import * as fs from 'fs'; import * as path from 'path'; @@ -34,8 +34,7 @@ export class LwcGenerationCommand { static async getSObjectsFromLandingPage(): Promise { return new Promise(async (resolve) => { - const staticResourcesPath = - await TemplateChooserCommand.getStaticResourcesDir(); + const staticResourcesPath = await UIUtils.getStaticResourcesDir(); const landingPageJson = 'landing_page.json'; const landingPagePath = path.join( staticResourcesPath, diff --git a/src/commands/wizard/templateChooserCommand.ts b/src/commands/wizard/templateChooserCommand.ts index 06b4ad4..92dac55 100644 --- a/src/commands/wizard/templateChooserCommand.ts +++ b/src/commands/wizard/templateChooserCommand.ts @@ -10,6 +10,7 @@ import { ProgressLocation, window, workspace } from 'vscode'; import * as path from 'path'; import { access, copyFile } from 'fs/promises'; import { InstructionsWebviewProvider } from '../../webviews/instructions'; +import { UIUtils } from '../../utils/uiUtils'; export type LandingPageStatus = { exists: boolean; @@ -35,12 +36,6 @@ export type LandingPageCollectionStatus = { * When the project is deployed to the user's org, this file will also be copied into static resources and picked up by SApp+. */ export class TemplateChooserCommand { - static readonly STATIC_RESOURCES_PATH = path.join( - 'force-app', - 'main', - 'default', - 'staticresources' - ); static readonly LANDING_PAGE_FILENAME_PREFIX = 'landing_page'; static readonly LANDING_PAGE_JSON_FILE_EXTENSION = '.json'; static readonly LANDING_PAGE_METADATA_FILE_EXTENSION = '.resource-meta.xml'; @@ -110,7 +105,7 @@ export class TemplateChooserCommand { } // If a landing page exists, warn about overwriting it. - const staticResourcesPath = await this.getStaticResourcesDir(); + const staticResourcesPath = await UIUtils.getStaticResourcesDir(); const existingLandingPageFiles = await this.landingPageFilesExist( staticResourcesPath, 'existing' @@ -186,7 +181,7 @@ export class TemplateChooserCommand { let staticResourcesPath: string; try { - staticResourcesPath = await this.getStaticResourcesDir(); + staticResourcesPath = await UIUtils.getStaticResourcesDir(); } catch (err) { landingPageCollectionStatus.error = (err as Error).message; return resolve(landingPageCollectionStatus); @@ -243,31 +238,6 @@ export class TemplateChooserCommand { return workspaceFolders[0].uri.fsPath; } - static async getStaticResourcesDir(): Promise { - return new Promise(async (resolve, reject) => { - let projectPath: string; - try { - projectPath = this.getWorkspaceDir(); - } catch (err) { - return reject(err); - } - const staticResourcesPath = path.join( - projectPath, - this.STATIC_RESOURCES_PATH - ); - try { - await access(staticResourcesPath); - } catch (err) { - const accessErrorObj = err as Error; - const noAccessError = new NoStaticResourcesDirError( - `Could not read landing page directory at '${staticResourcesPath}': ${accessErrorObj.message}` - ); - return reject(noAccessError); - } - return resolve(staticResourcesPath); - }); - } - static async landingPageFilesExist( staticResourcesPath: string, landingPageType: LandingPageType diff --git a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts index 371cf53..252b6ee 100644 --- a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts +++ b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts @@ -13,7 +13,7 @@ import { LwcGenerationCommand, SObjectQuickActionStatus } from '../../../../commands/wizard/lwcGenerationCommand'; -import { TemplateChooserCommand } from '../../../../commands/wizard/templateChooserCommand'; +import { UIUtils } from '../../../../utils/uiUtils'; suite('LWC Generation Command Test Suite', () => { beforeEach(function () {}); @@ -95,7 +95,7 @@ suite('LWC Generation Command Test Suite', () => { test('should return error status for landing page with invalid json', async () => { const getWorkspaceDirStub = sinon.stub( - TemplateChooserCommand, + UIUtils, 'getStaticResourcesDir' ); getWorkspaceDirStub.returns(Promise.resolve('.')); @@ -114,7 +114,7 @@ suite('LWC Generation Command Test Suite', () => { test('should return 2 sObjects', async () => { const getWorkspaceDirStub = sinon.stub( - TemplateChooserCommand, + UIUtils, 'getStaticResourcesDir' ); getWorkspaceDirStub.returns(Promise.resolve('.')); diff --git a/src/test/suite/commands/wizard/templateChooserCommand.test.ts b/src/test/suite/commands/wizard/templateChooserCommand.test.ts index 9e29403..bcc1632 100644 --- a/src/test/suite/commands/wizard/templateChooserCommand.test.ts +++ b/src/test/suite/commands/wizard/templateChooserCommand.test.ts @@ -8,16 +8,15 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as fs from 'fs'; -import { mkdir } from 'fs/promises'; import * as path from 'path'; +import { mkdir } from 'fs/promises'; import { afterEach, beforeEach } from 'mocha'; import { TemplateChooserCommand, - NoWorkspaceError, - NoStaticResourcesDirError, LandingPageType } from '../../../../commands/wizard/templateChooserCommand'; import { TempProjectDirManager } from '../../../TestHelper'; +import { UIUtils } from '../../../../utils/uiUtils'; type LandingPageTestIOConfig = { [landingPageType in LandingPageType]?: { @@ -32,76 +31,14 @@ suite('Template Chooser Command Test Suite', () => { sinon.restore(); }); - test('Static resources dir: workspace does not exist', async () => { - try { - await TemplateChooserCommand.getStaticResourcesDir(); - assert.fail('There should have been an error thrown.'); - } catch (noWorkspaceErr) { - assert.ok( - noWorkspaceErr instanceof NoWorkspaceError, - 'No workspace should be defined in this test.' - ); - } - }); - - test('Static resources dir: static resources dir does not exist', async () => { - const projectDirMgr = - await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub( - TemplateChooserCommand, - 'getWorkspaceDir' - ); - getWorkspaceDirStub.returns(projectDirMgr.projectDir); - try { - await TemplateChooserCommand.getStaticResourcesDir(); - assert.fail('There should have been an error thrown.'); - } catch (noStaticDirErr) { - assert.ok( - noStaticDirErr instanceof NoStaticResourcesDirError, - 'No static resources dir should be defined in this test.' - ); - } finally { - await projectDirMgr.removeDir(); - getWorkspaceDirStub.restore(); - } - }); - - test('Static resources dir: static resources dir exists', async () => { - const projectDirMgr = - await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub( - TemplateChooserCommand, - 'getWorkspaceDir' - ); - getWorkspaceDirStub.returns(projectDirMgr.projectDir); - - const staticResourcesAbsPath = path.join( - projectDirMgr.projectDir, - TemplateChooserCommand.STATIC_RESOURCES_PATH - ); - await mkdir(staticResourcesAbsPath, { recursive: true }); - - try { - const outputDir = - await TemplateChooserCommand.getStaticResourcesDir(); - assert.equal(outputDir, staticResourcesAbsPath); - } finally { - await projectDirMgr.removeDir(); - getWorkspaceDirStub.restore(); - } - }); - test('Landing pages exist: existing landing page file combinations', async () => { const projectDirMgr = await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub( - TemplateChooserCommand, - 'getWorkspaceDir' - ); + const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); getWorkspaceDirStub.returns(projectDirMgr.projectDir); const staticResourcesAbsPath = path.join( projectDirMgr.projectDir, - TemplateChooserCommand.STATIC_RESOURCES_PATH + UIUtils.STATIC_RESOURCES_PATH ); await mkdir(staticResourcesAbsPath, { recursive: true }); @@ -174,14 +111,11 @@ suite('Template Chooser Command Test Suite', () => { test('User is asked to overwrite existing landing page', async () => { const projectDirMgr = await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub( - TemplateChooserCommand, - 'getWorkspaceDir' - ); + const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); getWorkspaceDirStub.returns(projectDirMgr.projectDir); const staticResourcesAbsPath = path.join( projectDirMgr.projectDir, - TemplateChooserCommand.STATIC_RESOURCES_PATH + UIUtils.STATIC_RESOURCES_PATH ); await mkdir(staticResourcesAbsPath, { recursive: true }); const config: LandingPageTestIOConfig = { @@ -231,7 +165,7 @@ suite('Template Chooser Command Test Suite', () => { getWorkspaceDirStub.returns(projectDirMgr.projectDir); const staticResourcesAbsPath = path.join( projectDirMgr.projectDir, - TemplateChooserCommand.STATIC_RESOURCES_PATH + UIUtils.STATIC_RESOURCES_PATH ); await mkdir(staticResourcesAbsPath, { recursive: true }); @@ -299,14 +233,11 @@ suite('Template Chooser Command Test Suite', () => { test('Landing page status: various file existence scenarios', async () => { const projectDirMgr = await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub( - TemplateChooserCommand, - 'getWorkspaceDir' - ); + const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); getWorkspaceDirStub.returns(projectDirMgr.projectDir); const staticResourcesAbsPath = path.join( projectDirMgr.projectDir, - TemplateChooserCommand.STATIC_RESOURCES_PATH + UIUtils.STATIC_RESOURCES_PATH ); await mkdir(staticResourcesAbsPath, { recursive: true }); const landingPageConfig: LandingPageTestIOConfig = { diff --git a/src/test/suite/utils/uiUtils.test.ts b/src/test/suite/utils/uiUtils.test.ts index f09c096..ecba644 100644 --- a/src/test/suite/utils/uiUtils.test.ts +++ b/src/test/suite/utils/uiUtils.test.ts @@ -6,7 +6,14 @@ */ import * as assert from 'assert'; +import * as path from 'path'; +import { mkdir } from 'fs/promises'; import { UIUtils } from '../../../utils/uiUtils'; +import { + NoStaticResourcesDirError, + NoWorkspaceError +} from '../../../commands/wizard/templateChooserCommand'; +import { TempProjectDirManager } from '../../TestHelper'; import { QuickPickItem, window, QuickPick } from 'vscode'; import { afterEach, beforeEach } from 'mocha'; import sinon = require('sinon'); @@ -94,4 +101,56 @@ suite('UIUtils Test Suite', () => { assert.equal(exceptionCount, 1); assert.equal(quickPickSpy.dispose.called, true); // ensure it was disposed of after item selected }); + + test('Static resources dir: workspace does not exist', async () => { + try { + await UIUtils.getStaticResourcesDir(); + assert.fail('There should have been an error thrown.'); + } catch (noWorkspaceErr) { + assert.ok( + noWorkspaceErr instanceof NoWorkspaceError, + 'No workspace should be defined in this test.' + ); + } + }); + + test('Static resources dir: static resources dir does not exist', async () => { + const projectDirMgr = + await TempProjectDirManager.createTempProjectDir(); + const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); + getWorkspaceDirStub.returns(projectDirMgr.projectDir); + try { + await UIUtils.getStaticResourcesDir(); + assert.fail('There should have been an error thrown.'); + } catch (noStaticDirErr) { + assert.ok( + noStaticDirErr instanceof NoStaticResourcesDirError, + 'No static resources dir should be defined in this test.' + ); + } finally { + await projectDirMgr.removeDir(); + getWorkspaceDirStub.restore(); + } + }); + + test('Static resources dir: static resources dir exists', async () => { + const projectDirMgr = + await TempProjectDirManager.createTempProjectDir(); + const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); + getWorkspaceDirStub.returns(projectDirMgr.projectDir); + + const staticResourcesAbsPath = path.join( + projectDirMgr.projectDir, + UIUtils.STATIC_RESOURCES_PATH + ); + await mkdir(staticResourcesAbsPath, { recursive: true }); + + try { + const outputDir = await UIUtils.getStaticResourcesDir(); + assert.equal(outputDir, staticResourcesAbsPath); + } finally { + await projectDirMgr.removeDir(); + getWorkspaceDirStub.restore(); + } + }); }); diff --git a/src/utils/uiUtils.ts b/src/utils/uiUtils.ts index 5411974..280730d 100644 --- a/src/utils/uiUtils.ts +++ b/src/utils/uiUtils.ts @@ -5,12 +5,25 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { window, QuickPickItem, QuickPickItemKind, QuickPick } from 'vscode'; +import { window, workspace, QuickPickItem } from 'vscode'; +import { access } from 'fs/promises'; +import { + NoStaticResourcesDirError, + NoWorkspaceError +} from '../commands/wizard/templateChooserCommand'; +import * as path from 'path'; /** * Convenience wrapper for VS Code UI Extension methods such as showQuickPick(). */ export class UIUtils { + static readonly STATIC_RESOURCES_PATH = path.join( + 'force-app', + 'main', + 'default', + 'staticresources' + ); + /** * Wraps the ability to ask user for a selection from a quick pick list. * @@ -64,4 +77,39 @@ export class UIUtils { quickPick.show(); }); } + + static getWorkspaceDir(): string { + const workspaceFolders = workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + throw new NoWorkspaceError( + 'No workspace defined for this project.' + ); + } + return workspaceFolders[0].uri.fsPath; + } + + static async getStaticResourcesDir(): Promise { + return new Promise(async (resolve, reject) => { + let projectPath: string; + try { + projectPath = this.getWorkspaceDir(); + } catch (err) { + return reject(err); + } + const staticResourcesPath = path.join( + projectPath, + this.STATIC_RESOURCES_PATH + ); + try { + await access(staticResourcesPath); + } catch (err) { + const accessErrorObj = err as Error; + const noAccessError = new NoStaticResourcesDirError( + `Could not read landing page directory at '${staticResourcesPath}': ${accessErrorObj.message}` + ); + return reject(noAccessError); + } + return resolve(staticResourcesPath); + }); + } } From 2cd295d26f4314125b2dd06d94182361873fe832 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Thu, 9 Nov 2023 14:21:40 -0800 Subject: [PATCH 12/21] Move redundant check. --- src/utils/uemParser.ts | 46 ++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/utils/uemParser.ts b/src/utils/uemParser.ts index 0cffa40..0e3ca1b 100644 --- a/src/utils/uemParser.ts +++ b/src/utils/uemParser.ts @@ -35,35 +35,33 @@ export class UEMParser { ): Array { const results: Array = []; - if (typeof json === 'object') { - if (Array.isArray(json)) { - for (const item of json) { - results.push( - ...UEMParser.findObjectsWithValues(item, valuesToMatch) - ); - } - } else { - const values = Object.values(json); - - const matched = valuesToMatch.some((value) => - values.includes(value) + if (Array.isArray(json)) { + for (const item of json) { + results.push( + ...UEMParser.findObjectsWithValues(item, valuesToMatch) ); + } + } else { + const values = Object.values(json); - if (matched) { - results.push(json); - } + const matched = valuesToMatch.some((value) => + values.includes(value) + ); - for (const key in json) { - results.push( - ...UEMParser.findObjectsWithValues( - json[key as keyof Object], - valuesToMatch - ) - ); - } + if (matched) { + results.push(json); } - } + for (const key in json) { + results.push( + ...UEMParser.findObjectsWithValues( + json[key as keyof Object], + valuesToMatch + ) + ); + } + } + return results; } } From e0052a527e96b026fbf826c0467d91e444eef80e Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Fri, 10 Nov 2023 11:38:12 -0800 Subject: [PATCH 13/21] Recursion should only happen for nested objects and arrays. --- src/utils/uemParser.ts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/utils/uemParser.ts b/src/utils/uemParser.ts index 0e3ca1b..3eadfc6 100644 --- a/src/utils/uemParser.ts +++ b/src/utils/uemParser.ts @@ -30,36 +30,37 @@ export class UEMParser { } static findObjectsWithValues( - json: Object, + nestedJsonBlock: any, valuesToMatch: string[] - ): Array { - const results: Array = []; + ): Array { + const results: Array = []; - if (Array.isArray(json)) { - for (const item of json) { - results.push( - ...UEMParser.findObjectsWithValues(item, valuesToMatch) - ); - } - } else { - const values = Object.values(json); + if (typeof nestedJsonBlock === 'object') { + const values = Object.values(nestedJsonBlock); const matched = valuesToMatch.some((value) => values.includes(value) ); if (matched) { - results.push(json); + results.push(nestedJsonBlock); } - for (const key in json) { + for (const key in nestedJsonBlock) { results.push( ...UEMParser.findObjectsWithValues( - json[key as keyof Object], + nestedJsonBlock[key as keyof Object], valuesToMatch ) ); } + } else if (Array.isArray(nestedJsonBlock)) { + const nestedArrayBlock = nestedJsonBlock as Array; + for (const item of nestedArrayBlock) { + results.push( + ...UEMParser.findObjectsWithValues(item, valuesToMatch) + ); + } } return results; From 28b161df15adb2ab21f5b62cc6acb9c2a424dd21 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Fri, 10 Nov 2023 11:39:26 -0800 Subject: [PATCH 14/21] nit. --- src/utils/uemParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/uemParser.ts b/src/utils/uemParser.ts index 3eadfc6..16c3b7b 100644 --- a/src/utils/uemParser.ts +++ b/src/utils/uemParser.ts @@ -62,7 +62,7 @@ export class UEMParser { ); } } - + return results; } } From fadfedc0ef9c38f1b0210b14b9b0fdd5fe3b1157 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Fri, 10 Nov 2023 21:04:43 -0800 Subject: [PATCH 15/21] Simple search. Look for all objectApiName. --- .../wizard/lwcGenerationCommand.test.ts | 4 +- src/utils/uemParser.ts | 63 +++++++------------ 2 files changed, 23 insertions(+), 44 deletions(-) diff --git a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts index 252b6ee..bf4c0e7 100644 --- a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts +++ b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts @@ -93,7 +93,7 @@ suite('LWC Generation Command Test Suite', () => { ); }); - test('should return error status for landing page with invalid json', async () => { + test('Should return error status for landing page with invalid json', async () => { const getWorkspaceDirStub = sinon.stub( UIUtils, 'getStaticResourcesDir' @@ -112,7 +112,7 @@ suite('LWC Generation Command Test Suite', () => { fs.unlinkSync(invalidJsonFile); }); - test('should return 2 sObjects', async () => { + test('Should return 2 sObjects', async () => { const getWorkspaceDirStub = sinon.stub( UIUtils, 'getStaticResourcesDir' diff --git a/src/utils/uemParser.ts b/src/utils/uemParser.ts index 16c3b7b..fe6e8c2 100644 --- a/src/utils/uemParser.ts +++ b/src/utils/uemParser.ts @@ -7,62 +7,41 @@ export class UEMParser { public static findSObjects(json: Object): Array { - const sObjects = UEMParser.findObjectsWithValues(json, [ - 'mcf/list', - 'mcf/timedList', - 'mcf/genericLists' - ]); - const results: string[] = []; + const sObjects = UEMParser.findObjectsWithKey(json, 'objectApiName'); - sObjects.forEach((obj) => { - let properties = obj['properties' as keyof Object]; - let objectApiName = properties[ - 'objectApiName' as keyof Object - ] as unknown as string; - - // Only include unique values in the array. - if (!results.includes(objectApiName)) { - results.push(objectApiName); - } - }); - - return results; + return sObjects; } - static findObjectsWithValues( + static findObjectsWithKey( nestedJsonBlock: any, - valuesToMatch: string[] - ): Array { + keyToMatch: string + ): Array { const results: Array = []; if (typeof nestedJsonBlock === 'object') { - const values = Object.values(nestedJsonBlock); - - const matched = valuesToMatch.some((value) => - values.includes(value) - ); - - if (matched) { - results.push(nestedJsonBlock); - } + const keys = Object.keys(nestedJsonBlock); for (const key in nestedJsonBlock) { - results.push( - ...UEMParser.findObjectsWithValues( - nestedJsonBlock[key as keyof Object], - valuesToMatch - ) - ); + const value = nestedJsonBlock[key]; + if (key === keyToMatch && typeof value === 'string') { + results.push(nestedJsonBlock[key as keyof Object]); + } else { + results.push( + ...UEMParser.findObjectsWithKey( + nestedJsonBlock[key as keyof Object], + keyToMatch + ) + ); + } } } else if (Array.isArray(nestedJsonBlock)) { - const nestedArrayBlock = nestedJsonBlock as Array; + const nestedArrayBlock = nestedJsonBlock as Array; for (const item of nestedArrayBlock) { - results.push( - ...UEMParser.findObjectsWithValues(item, valuesToMatch) - ); + results.push(...UEMParser.findObjectsWithKey(item, keyToMatch)); } } - return results; + // Clean the array to return. Remove duplicate values. + return [...new Set(results)]; } } From 73cc87eeb145bcd4cad74909501efcaf3f467b5e Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Mon, 13 Nov 2023 12:04:57 -0800 Subject: [PATCH 16/21] Updates after code review. --- src/test/suite/utils/uiUtils.test.ts | 2 +- src/utils/uemParser.ts | 11 ++--------- src/utils/uiUtils.ts | 6 +++--- src/utils/workspaceUtils.ts | 22 ++++++++++++++++++++++ 4 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 src/utils/workspaceUtils.ts diff --git a/src/test/suite/utils/uiUtils.test.ts b/src/test/suite/utils/uiUtils.test.ts index ecba644..5bee097 100644 --- a/src/test/suite/utils/uiUtils.test.ts +++ b/src/test/suite/utils/uiUtils.test.ts @@ -12,7 +12,7 @@ import { UIUtils } from '../../../utils/uiUtils'; import { NoStaticResourcesDirError, NoWorkspaceError -} from '../../../commands/wizard/templateChooserCommand'; +} from '../../../utils/workspaceUtils'; import { TempProjectDirManager } from '../../TestHelper'; import { QuickPickItem, window, QuickPick } from 'vscode'; import { afterEach, beforeEach } from 'mocha'; diff --git a/src/utils/uemParser.ts b/src/utils/uemParser.ts index fe6e8c2..9d7e0d3 100644 --- a/src/utils/uemParser.ts +++ b/src/utils/uemParser.ts @@ -16,11 +16,9 @@ export class UEMParser { nestedJsonBlock: any, keyToMatch: string ): Array { - const results: Array = []; + const results: Array = []; if (typeof nestedJsonBlock === 'object') { - const keys = Object.keys(nestedJsonBlock); - for (const key in nestedJsonBlock) { const value = nestedJsonBlock[key]; if (key === keyToMatch && typeof value === 'string') { @@ -34,12 +32,7 @@ export class UEMParser { ); } } - } else if (Array.isArray(nestedJsonBlock)) { - const nestedArrayBlock = nestedJsonBlock as Array; - for (const item of nestedArrayBlock) { - results.push(...UEMParser.findObjectsWithKey(item, keyToMatch)); - } - } + } // Clean the array to return. Remove duplicate values. return [...new Set(results)]; diff --git a/src/utils/uiUtils.ts b/src/utils/uiUtils.ts index 280730d..9029161 100644 --- a/src/utils/uiUtils.ts +++ b/src/utils/uiUtils.ts @@ -10,7 +10,7 @@ import { access } from 'fs/promises'; import { NoStaticResourcesDirError, NoWorkspaceError -} from '../commands/wizard/templateChooserCommand'; +} from './workspaceUtils'; import * as path from 'path'; /** @@ -103,10 +103,10 @@ export class UIUtils { try { await access(staticResourcesPath); } catch (err) { - const accessErrorObj = err as Error; const noAccessError = new NoStaticResourcesDirError( - `Could not read landing page directory at '${staticResourcesPath}': ${accessErrorObj.message}` + `Could not read static resources directory at '${staticResourcesPath}'` ); + return reject(noAccessError); } return resolve(staticResourcesPath); diff --git a/src/utils/workspaceUtils.ts b/src/utils/workspaceUtils.ts new file mode 100644 index 0000000..00d5cd9 --- /dev/null +++ b/src/utils/workspaceUtils.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +export class NoWorkspaceError extends Error { + constructor(message?: string) { + super(message); + this.name = this.constructor.name; + Object.setPrototypeOf(this, NoWorkspaceError.prototype); + } +} + +export class NoStaticResourcesDirError extends Error { + constructor(message?: string) { + super(message); + this.name = this.constructor.name; + Object.setPrototypeOf(this, NoStaticResourcesDirError.prototype); + } +} From c9fe7c3f8c179ab631369644761180907afa4a9a Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Mon, 13 Nov 2023 12:07:03 -0800 Subject: [PATCH 17/21] Prettier. --- src/utils/uemParser.ts | 2 +- src/utils/uiUtils.ts | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/utils/uemParser.ts b/src/utils/uemParser.ts index 9d7e0d3..cd53bd9 100644 --- a/src/utils/uemParser.ts +++ b/src/utils/uemParser.ts @@ -32,7 +32,7 @@ export class UEMParser { ); } } - } + } // Clean the array to return. Remove duplicate values. return [...new Set(results)]; diff --git a/src/utils/uiUtils.ts b/src/utils/uiUtils.ts index 9029161..8cc3110 100644 --- a/src/utils/uiUtils.ts +++ b/src/utils/uiUtils.ts @@ -7,10 +7,7 @@ import { window, workspace, QuickPickItem } from 'vscode'; import { access } from 'fs/promises'; -import { - NoStaticResourcesDirError, - NoWorkspaceError -} from './workspaceUtils'; +import { NoStaticResourcesDirError, NoWorkspaceError } from './workspaceUtils'; import * as path from 'path'; /** @@ -106,7 +103,7 @@ export class UIUtils { const noAccessError = new NoStaticResourcesDirError( `Could not read static resources directory at '${staticResourcesPath}'` ); - + return reject(noAccessError); } return resolve(staticResourcesPath); From d106e9a2be45d200e42e4c5c7a9ab5cab8eff29c Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Mon, 13 Nov 2023 13:41:52 -0800 Subject: [PATCH 18/21] Stub. --- src/test/suite/commands/wizard/templateChooserCommand.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/suite/commands/wizard/templateChooserCommand.test.ts b/src/test/suite/commands/wizard/templateChooserCommand.test.ts index bcc1632..78fd785 100644 --- a/src/test/suite/commands/wizard/templateChooserCommand.test.ts +++ b/src/test/suite/commands/wizard/templateChooserCommand.test.ts @@ -159,7 +159,7 @@ suite('Template Chooser Command Test Suite', () => { const projectDirMgr = await TempProjectDirManager.createTempProjectDir(); const getWorkspaceDirStub = sinon.stub( - TemplateChooserCommand, + UIUtils, 'getWorkspaceDir' ); getWorkspaceDirStub.returns(projectDirMgr.projectDir); From faf372358d5c50ebe71f223dbdea1433491cbb55 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Mon, 13 Nov 2023 13:50:21 -0800 Subject: [PATCH 19/21] Prettier. --- .../suite/commands/wizard/templateChooserCommand.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/suite/commands/wizard/templateChooserCommand.test.ts b/src/test/suite/commands/wizard/templateChooserCommand.test.ts index 78fd785..b840e08 100644 --- a/src/test/suite/commands/wizard/templateChooserCommand.test.ts +++ b/src/test/suite/commands/wizard/templateChooserCommand.test.ts @@ -158,10 +158,7 @@ suite('Template Chooser Command Test Suite', () => { test('Landing page template written to landing page files', async () => { const projectDirMgr = await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub( - UIUtils, - 'getWorkspaceDir' - ); + const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); getWorkspaceDirStub.returns(projectDirMgr.projectDir); const staticResourcesAbsPath = path.join( projectDirMgr.projectDir, From 2710bb0eb787b1dc43447fd5853bddefccba892c Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Tue, 14 Nov 2023 14:12:31 -0800 Subject: [PATCH 20/21] Move methods to WorkspaceUtils class. --- src/commands/wizard/lwcGenerationCommand.ts | 5 +- src/commands/wizard/templateChooserCommand.ts | 8 ++-- .../wizard/lwcGenerationCommand.test.ts | 6 +-- .../wizard/templateChooserCommand.test.ts | 29 +++++++---- src/test/suite/utils/uiUtils.test.ts | 21 +++++--- src/utils/uiUtils.ts | 47 +----------------- src/utils/workspaceUtils.ts | 48 +++++++++++++++++++ 7 files changed, 95 insertions(+), 69 deletions(-) diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 3b122a4..bf8092e 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -2,7 +2,7 @@ import { Uri, l10n } from 'vscode'; import { access } from 'fs/promises'; import { InstructionsWebviewProvider } from '../../webviews/instructions'; import { UEMParser } from '../../utils/uemParser'; -import { UIUtils } from '../../utils/uiUtils'; +import { WorkspaceUtils } from '../../utils/workspaceUtils'; import { CommonUtils } from '@salesforce/lwc-dev-mobile-core'; import * as fs from 'fs'; import * as path from 'path'; @@ -34,7 +34,8 @@ export class LwcGenerationCommand { static async getSObjectsFromLandingPage(): Promise { return new Promise(async (resolve) => { - const staticResourcesPath = await UIUtils.getStaticResourcesDir(); + const staticResourcesPath = + await WorkspaceUtils.getStaticResourcesDir(); const landingPageJson = 'landing_page.json'; const landingPagePath = path.join( staticResourcesPath, diff --git a/src/commands/wizard/templateChooserCommand.ts b/src/commands/wizard/templateChooserCommand.ts index 92dac55..4b9367d 100644 --- a/src/commands/wizard/templateChooserCommand.ts +++ b/src/commands/wizard/templateChooserCommand.ts @@ -10,7 +10,7 @@ import { ProgressLocation, window, workspace } from 'vscode'; import * as path from 'path'; import { access, copyFile } from 'fs/promises'; import { InstructionsWebviewProvider } from '../../webviews/instructions'; -import { UIUtils } from '../../utils/uiUtils'; +import { WorkspaceUtils } from '../../utils/workspaceUtils'; export type LandingPageStatus = { exists: boolean; @@ -105,7 +105,8 @@ export class TemplateChooserCommand { } // If a landing page exists, warn about overwriting it. - const staticResourcesPath = await UIUtils.getStaticResourcesDir(); + const staticResourcesPath = + await WorkspaceUtils.getStaticResourcesDir(); const existingLandingPageFiles = await this.landingPageFilesExist( staticResourcesPath, 'existing' @@ -181,7 +182,8 @@ export class TemplateChooserCommand { let staticResourcesPath: string; try { - staticResourcesPath = await UIUtils.getStaticResourcesDir(); + staticResourcesPath = + await WorkspaceUtils.getStaticResourcesDir(); } catch (err) { landingPageCollectionStatus.error = (err as Error).message; return resolve(landingPageCollectionStatus); diff --git a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts index bf4c0e7..88a50a6 100644 --- a/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts +++ b/src/test/suite/commands/wizard/lwcGenerationCommand.test.ts @@ -13,7 +13,7 @@ import { LwcGenerationCommand, SObjectQuickActionStatus } from '../../../../commands/wizard/lwcGenerationCommand'; -import { UIUtils } from '../../../../utils/uiUtils'; +import { WorkspaceUtils } from '../../../../utils/workspaceUtils'; suite('LWC Generation Command Test Suite', () => { beforeEach(function () {}); @@ -95,7 +95,7 @@ suite('LWC Generation Command Test Suite', () => { test('Should return error status for landing page with invalid json', async () => { const getWorkspaceDirStub = sinon.stub( - UIUtils, + WorkspaceUtils, 'getStaticResourcesDir' ); getWorkspaceDirStub.returns(Promise.resolve('.')); @@ -114,7 +114,7 @@ suite('LWC Generation Command Test Suite', () => { test('Should return 2 sObjects', async () => { const getWorkspaceDirStub = sinon.stub( - UIUtils, + WorkspaceUtils, 'getStaticResourcesDir' ); getWorkspaceDirStub.returns(Promise.resolve('.')); diff --git a/src/test/suite/commands/wizard/templateChooserCommand.test.ts b/src/test/suite/commands/wizard/templateChooserCommand.test.ts index b840e08..3e762bf 100644 --- a/src/test/suite/commands/wizard/templateChooserCommand.test.ts +++ b/src/test/suite/commands/wizard/templateChooserCommand.test.ts @@ -17,6 +17,7 @@ import { } from '../../../../commands/wizard/templateChooserCommand'; import { TempProjectDirManager } from '../../../TestHelper'; import { UIUtils } from '../../../../utils/uiUtils'; +import { WorkspaceUtils } from '../../../../utils/workspaceUtils'; type LandingPageTestIOConfig = { [landingPageType in LandingPageType]?: { @@ -34,11 +35,14 @@ suite('Template Chooser Command Test Suite', () => { test('Landing pages exist: existing landing page file combinations', async () => { const projectDirMgr = await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); + const getWorkspaceDirStub = sinon.stub( + WorkspaceUtils, + 'getWorkspaceDir' + ); getWorkspaceDirStub.returns(projectDirMgr.projectDir); const staticResourcesAbsPath = path.join( projectDirMgr.projectDir, - UIUtils.STATIC_RESOURCES_PATH + WorkspaceUtils.STATIC_RESOURCES_PATH ); await mkdir(staticResourcesAbsPath, { recursive: true }); @@ -111,11 +115,14 @@ suite('Template Chooser Command Test Suite', () => { test('User is asked to overwrite existing landing page', async () => { const projectDirMgr = await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); + const getWorkspaceDirStub = sinon.stub( + WorkspaceUtils, + 'getWorkspaceDir' + ); getWorkspaceDirStub.returns(projectDirMgr.projectDir); const staticResourcesAbsPath = path.join( projectDirMgr.projectDir, - UIUtils.STATIC_RESOURCES_PATH + WorkspaceUtils.STATIC_RESOURCES_PATH ); await mkdir(staticResourcesAbsPath, { recursive: true }); const config: LandingPageTestIOConfig = { @@ -158,11 +165,14 @@ suite('Template Chooser Command Test Suite', () => { test('Landing page template written to landing page files', async () => { const projectDirMgr = await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); + const getWorkspaceDirStub = sinon.stub( + WorkspaceUtils, + 'getWorkspaceDir' + ); getWorkspaceDirStub.returns(projectDirMgr.projectDir); const staticResourcesAbsPath = path.join( projectDirMgr.projectDir, - UIUtils.STATIC_RESOURCES_PATH + WorkspaceUtils.STATIC_RESOURCES_PATH ); await mkdir(staticResourcesAbsPath, { recursive: true }); @@ -230,11 +240,14 @@ suite('Template Chooser Command Test Suite', () => { test('Landing page status: various file existence scenarios', async () => { const projectDirMgr = await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); + const getWorkspaceDirStub = sinon.stub( + WorkspaceUtils, + 'getWorkspaceDir' + ); getWorkspaceDirStub.returns(projectDirMgr.projectDir); const staticResourcesAbsPath = path.join( projectDirMgr.projectDir, - UIUtils.STATIC_RESOURCES_PATH + WorkspaceUtils.STATIC_RESOURCES_PATH ); await mkdir(staticResourcesAbsPath, { recursive: true }); const landingPageConfig: LandingPageTestIOConfig = { diff --git a/src/test/suite/utils/uiUtils.test.ts b/src/test/suite/utils/uiUtils.test.ts index 5bee097..d47d4b1 100644 --- a/src/test/suite/utils/uiUtils.test.ts +++ b/src/test/suite/utils/uiUtils.test.ts @@ -11,7 +11,8 @@ import { mkdir } from 'fs/promises'; import { UIUtils } from '../../../utils/uiUtils'; import { NoStaticResourcesDirError, - NoWorkspaceError + NoWorkspaceError, + WorkspaceUtils } from '../../../utils/workspaceUtils'; import { TempProjectDirManager } from '../../TestHelper'; import { QuickPickItem, window, QuickPick } from 'vscode'; @@ -104,7 +105,7 @@ suite('UIUtils Test Suite', () => { test('Static resources dir: workspace does not exist', async () => { try { - await UIUtils.getStaticResourcesDir(); + await WorkspaceUtils.getStaticResourcesDir(); assert.fail('There should have been an error thrown.'); } catch (noWorkspaceErr) { assert.ok( @@ -117,10 +118,13 @@ suite('UIUtils Test Suite', () => { test('Static resources dir: static resources dir does not exist', async () => { const projectDirMgr = await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); + const getWorkspaceDirStub = sinon.stub( + WorkspaceUtils, + 'getWorkspaceDir' + ); getWorkspaceDirStub.returns(projectDirMgr.projectDir); try { - await UIUtils.getStaticResourcesDir(); + await WorkspaceUtils.getStaticResourcesDir(); assert.fail('There should have been an error thrown.'); } catch (noStaticDirErr) { assert.ok( @@ -136,17 +140,20 @@ suite('UIUtils Test Suite', () => { test('Static resources dir: static resources dir exists', async () => { const projectDirMgr = await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub(UIUtils, 'getWorkspaceDir'); + const getWorkspaceDirStub = sinon.stub( + WorkspaceUtils, + 'getWorkspaceDir' + ); getWorkspaceDirStub.returns(projectDirMgr.projectDir); const staticResourcesAbsPath = path.join( projectDirMgr.projectDir, - UIUtils.STATIC_RESOURCES_PATH + WorkspaceUtils.STATIC_RESOURCES_PATH ); await mkdir(staticResourcesAbsPath, { recursive: true }); try { - const outputDir = await UIUtils.getStaticResourcesDir(); + const outputDir = await WorkspaceUtils.getStaticResourcesDir(); assert.equal(outputDir, staticResourcesAbsPath); } finally { await projectDirMgr.removeDir(); diff --git a/src/utils/uiUtils.ts b/src/utils/uiUtils.ts index 8cc3110..de8be4f 100644 --- a/src/utils/uiUtils.ts +++ b/src/utils/uiUtils.ts @@ -5,22 +5,12 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { window, workspace, QuickPickItem } from 'vscode'; -import { access } from 'fs/promises'; -import { NoStaticResourcesDirError, NoWorkspaceError } from './workspaceUtils'; -import * as path from 'path'; +import { window, QuickPickItem } from 'vscode'; /** * Convenience wrapper for VS Code UI Extension methods such as showQuickPick(). */ export class UIUtils { - static readonly STATIC_RESOURCES_PATH = path.join( - 'force-app', - 'main', - 'default', - 'staticresources' - ); - /** * Wraps the ability to ask user for a selection from a quick pick list. * @@ -74,39 +64,4 @@ export class UIUtils { quickPick.show(); }); } - - static getWorkspaceDir(): string { - const workspaceFolders = workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { - throw new NoWorkspaceError( - 'No workspace defined for this project.' - ); - } - return workspaceFolders[0].uri.fsPath; - } - - static async getStaticResourcesDir(): Promise { - return new Promise(async (resolve, reject) => { - let projectPath: string; - try { - projectPath = this.getWorkspaceDir(); - } catch (err) { - return reject(err); - } - const staticResourcesPath = path.join( - projectPath, - this.STATIC_RESOURCES_PATH - ); - try { - await access(staticResourcesPath); - } catch (err) { - const noAccessError = new NoStaticResourcesDirError( - `Could not read static resources directory at '${staticResourcesPath}'` - ); - - return reject(noAccessError); - } - return resolve(staticResourcesPath); - }); - } } diff --git a/src/utils/workspaceUtils.ts b/src/utils/workspaceUtils.ts index 00d5cd9..cfdf3a2 100644 --- a/src/utils/workspaceUtils.ts +++ b/src/utils/workspaceUtils.ts @@ -5,6 +5,54 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ +import { workspace } from 'vscode'; +import { access } from 'fs/promises'; +import * as path from 'path'; + +export class WorkspaceUtils { + static readonly STATIC_RESOURCES_PATH = path.join( + 'force-app', + 'main', + 'default', + 'staticresources' + ); + + static getWorkspaceDir(): string { + const workspaceFolders = workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + throw new NoWorkspaceError( + 'No workspace defined for this project.' + ); + } + return workspaceFolders[0].uri.fsPath; + } + + static async getStaticResourcesDir(): Promise { + return new Promise(async (resolve, reject) => { + let projectPath: string; + try { + projectPath = this.getWorkspaceDir(); + } catch (err) { + return reject(err); + } + const staticResourcesPath = path.join( + projectPath, + this.STATIC_RESOURCES_PATH + ); + try { + await access(staticResourcesPath); + } catch (err) { + const noAccessError = new NoStaticResourcesDirError( + `Could not read static resources directory at '${staticResourcesPath}'` + ); + + return reject(noAccessError); + } + return resolve(staticResourcesPath); + }); + } +} + export class NoWorkspaceError extends Error { constructor(message?: string) { super(message); From 8f19fe38ae816d630ecd414d162fbd70398ab9e5 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Tue, 14 Nov 2023 14:22:15 -0800 Subject: [PATCH 21/21] Move tests. --- src/test/suite/utils/uiUtils.test.ts | 66 ---------------- src/test/suite/utils/workspaceUtils.test.ts | 84 +++++++++++++++++++++ 2 files changed, 84 insertions(+), 66 deletions(-) create mode 100644 src/test/suite/utils/workspaceUtils.test.ts diff --git a/src/test/suite/utils/uiUtils.test.ts b/src/test/suite/utils/uiUtils.test.ts index d47d4b1..f09c096 100644 --- a/src/test/suite/utils/uiUtils.test.ts +++ b/src/test/suite/utils/uiUtils.test.ts @@ -6,15 +6,7 @@ */ import * as assert from 'assert'; -import * as path from 'path'; -import { mkdir } from 'fs/promises'; import { UIUtils } from '../../../utils/uiUtils'; -import { - NoStaticResourcesDirError, - NoWorkspaceError, - WorkspaceUtils -} from '../../../utils/workspaceUtils'; -import { TempProjectDirManager } from '../../TestHelper'; import { QuickPickItem, window, QuickPick } from 'vscode'; import { afterEach, beforeEach } from 'mocha'; import sinon = require('sinon'); @@ -102,62 +94,4 @@ suite('UIUtils Test Suite', () => { assert.equal(exceptionCount, 1); assert.equal(quickPickSpy.dispose.called, true); // ensure it was disposed of after item selected }); - - test('Static resources dir: workspace does not exist', async () => { - try { - await WorkspaceUtils.getStaticResourcesDir(); - assert.fail('There should have been an error thrown.'); - } catch (noWorkspaceErr) { - assert.ok( - noWorkspaceErr instanceof NoWorkspaceError, - 'No workspace should be defined in this test.' - ); - } - }); - - test('Static resources dir: static resources dir does not exist', async () => { - const projectDirMgr = - await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub( - WorkspaceUtils, - 'getWorkspaceDir' - ); - getWorkspaceDirStub.returns(projectDirMgr.projectDir); - try { - await WorkspaceUtils.getStaticResourcesDir(); - assert.fail('There should have been an error thrown.'); - } catch (noStaticDirErr) { - assert.ok( - noStaticDirErr instanceof NoStaticResourcesDirError, - 'No static resources dir should be defined in this test.' - ); - } finally { - await projectDirMgr.removeDir(); - getWorkspaceDirStub.restore(); - } - }); - - test('Static resources dir: static resources dir exists', async () => { - const projectDirMgr = - await TempProjectDirManager.createTempProjectDir(); - const getWorkspaceDirStub = sinon.stub( - WorkspaceUtils, - 'getWorkspaceDir' - ); - getWorkspaceDirStub.returns(projectDirMgr.projectDir); - - const staticResourcesAbsPath = path.join( - projectDirMgr.projectDir, - WorkspaceUtils.STATIC_RESOURCES_PATH - ); - await mkdir(staticResourcesAbsPath, { recursive: true }); - - try { - const outputDir = await WorkspaceUtils.getStaticResourcesDir(); - assert.equal(outputDir, staticResourcesAbsPath); - } finally { - await projectDirMgr.removeDir(); - getWorkspaceDirStub.restore(); - } - }); }); diff --git a/src/test/suite/utils/workspaceUtils.test.ts b/src/test/suite/utils/workspaceUtils.test.ts new file mode 100644 index 0000000..2c49aaf --- /dev/null +++ b/src/test/suite/utils/workspaceUtils.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +import * as assert from 'assert'; +import * as path from 'path'; +import { mkdir } from 'fs/promises'; +import { + NoStaticResourcesDirError, + NoWorkspaceError, + WorkspaceUtils +} from '../../../utils/workspaceUtils'; +import { TempProjectDirManager } from '../../TestHelper'; +import { afterEach, beforeEach } from 'mocha'; +import sinon = require('sinon'); + +suite('Workspace Test Suite', () => { + beforeEach(function () {}); + + afterEach(function () { + sinon.restore(); + }); + + test('Static resources dir: workspace does not exist', async () => { + try { + await WorkspaceUtils.getStaticResourcesDir(); + assert.fail('There should have been an error thrown.'); + } catch (noWorkspaceErr) { + assert.ok( + noWorkspaceErr instanceof NoWorkspaceError, + 'No workspace should be defined in this test.' + ); + } + }); + + test('Static resources dir: static resources dir does not exist', async () => { + const projectDirMgr = + await TempProjectDirManager.createTempProjectDir(); + const getWorkspaceDirStub = sinon.stub( + WorkspaceUtils, + 'getWorkspaceDir' + ); + getWorkspaceDirStub.returns(projectDirMgr.projectDir); + try { + await WorkspaceUtils.getStaticResourcesDir(); + assert.fail('There should have been an error thrown.'); + } catch (noStaticDirErr) { + assert.ok( + noStaticDirErr instanceof NoStaticResourcesDirError, + 'No static resources dir should be defined in this test.' + ); + } finally { + await projectDirMgr.removeDir(); + getWorkspaceDirStub.restore(); + } + }); + + test('Static resources dir: static resources dir exists', async () => { + const projectDirMgr = + await TempProjectDirManager.createTempProjectDir(); + const getWorkspaceDirStub = sinon.stub( + WorkspaceUtils, + 'getWorkspaceDir' + ); + getWorkspaceDirStub.returns(projectDirMgr.projectDir); + + const staticResourcesAbsPath = path.join( + projectDirMgr.projectDir, + WorkspaceUtils.STATIC_RESOURCES_PATH + ); + await mkdir(staticResourcesAbsPath, { recursive: true }); + + try { + const outputDir = await WorkspaceUtils.getStaticResourcesDir(); + assert.equal(outputDir, staticResourcesAbsPath); + } finally { + await projectDirMgr.removeDir(); + getWorkspaceDirStub.restore(); + } + }); +});