diff --git a/resources/instructions/createSObjectLwcQuickActions.html b/resources/instructions/createSObjectLwcQuickActions.html new file mode 100644 index 00000000..33fe42e5 --- /dev/null +++ b/resources/instructions/createSObjectLwcQuickActions.html @@ -0,0 +1,81 @@ + + + + + + Offline Starter Kit: Create sObject LWC Quick Actions + + + +

Create sObject LWC Quick Actions

+ +

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

+ + + + + + + + + + + + + + + + + + + + + + +
sObjectLWC Quick Actions
Contact
Account
Opportunity
+ + + + + + + diff --git a/resources/instructions/landingPageTemplateChoice.html b/resources/instructions/landingPageTemplateChoice.html new file mode 100644 index 00000000..80d83391 --- /dev/null +++ b/resources/instructions/landingPageTemplateChoice.html @@ -0,0 +1,262 @@ + + + + + + Offline Starter Kit: Select Landing Page + + + +

Select a Landing Page Template

+ + +
+ + + + + + + + + +
+ + + + + diff --git a/src/commands/wizard/landingPageCommand.ts b/src/commands/wizard/landingPageCommand.ts index 2561a2fd..972ae55d 100644 --- a/src/commands/wizard/landingPageCommand.ts +++ b/src/commands/wizard/landingPageCommand.ts @@ -71,7 +71,7 @@ export class LandingPageCommand { } /** - * A Record List card shows a customized card for a particular SObject. It needs the following params from the user: + * A Record List card shows a customized card for a particular sObject. It needs the following params from the user: * - Primary, and optionally Secondary and Tertiary fields * - OrderBy field * - OrderBy direction (Ascending or Descending) diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts new file mode 100644 index 00000000..e3d3cc98 --- /dev/null +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -0,0 +1,30 @@ +import { Uri, l10n } from 'vscode'; +import { InstructionsWebviewProvider } from '../../webviews/instructions'; + +export class LwcGenerationCommand { + extensionUri: Uri; + + constructor(extensionUri: Uri) { + this.extensionUri = extensionUri; + } + + async createSObjectLwcQuickActions() { + return new Promise((resolve) => { + new InstructionsWebviewProvider( + this.extensionUri + ).showInstructionWebview( + l10n.t('Offline Starter Kit: Create sObject LWC Quick Actions'), + 'resources/instructions/createSObjectLwcQuickActions.html', + [ + { + type: 'generateLwcQuickActionsButton', + action: (panel) => { + panel.dispose(); + return resolve(); + } + } + ] + ); + }); + } +} diff --git a/src/commands/wizard/onboardingWizard.ts b/src/commands/wizard/onboardingWizard.ts index e2c90057..f128aeb7 100644 --- a/src/commands/wizard/onboardingWizard.ts +++ b/src/commands/wizard/onboardingWizard.ts @@ -12,6 +12,7 @@ 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 = @@ -27,7 +28,7 @@ async function runPostProjectConfigurationSteps( return new Promise(async (resolve) => { await AuthorizeCommand.authorizeToOrg(); await BriefcaseCommand.setupBriefcase(extensionUri); - await TemplateChooserCommand.copyDefaultTemplate(extensionUri); + await TemplateChooserCommand.chooseTemplate(extensionUri); await AuthorizeCommand.authorizeToOrg(); await DeployToOrgCommand.deployToOrg(); diff --git a/src/commands/wizard/templateChooserCommand.ts b/src/commands/wizard/templateChooserCommand.ts index b61576ad..06b4ad45 100644 --- a/src/commands/wizard/templateChooserCommand.ts +++ b/src/commands/wizard/templateChooserCommand.ts @@ -5,125 +5,321 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { QuickPickItem, Uri, l10n } from 'vscode'; -import { UIUtils } from '../../utils/uiUtils'; -import { workspace } from 'vscode'; +import { Uri, l10n } from 'vscode'; +import { ProgressLocation, window, workspace } from 'vscode'; import * as path from 'path'; -import * as fs from 'fs'; +import { access, copyFile } from 'fs/promises'; import { InstructionsWebviewProvider } from '../../webviews/instructions'; -export interface TemplateQuickPickItem extends QuickPickItem { - filenamePrefix: string; -} +export type LandingPageStatus = { + exists: boolean; + warning?: string; +}; + +export type LandingPageType = + | 'existing' + | 'default' + | 'caseManagement' + | 'healthcare' + | 'retail'; + +export type LandingPageCollectionStatus = { + error?: string; + landingPageCollection: { + [landingPageType in LandingPageType]?: LandingPageStatus; + }; +}; /** * This command will prompt the user to select one of the canned landing page templates, and will simply copy it to "landing_page.json". * 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 = - '/force-app/main/default/staticresources'; - static readonly LANDING_PAGE_DESTINATION_FILENAME_PREFIX = 'landing_page'; - static readonly JSON_FILE_EXTENSION = '.json'; - static readonly METADATA_FILE_EXTENSION = '.resource-meta.xml'; - - static readonly TEMPLATE_LIST_ITEMS: TemplateQuickPickItem[] = [ - { - label: l10n.t('Default'), - detail: l10n.t( - 'Recently viewed Contacts, Accounts, and Opportunities.' - ), - filenamePrefix: 'landing_page_default' - }, - { - label: l10n.t('Case Management'), - detail: l10n.t( - 'New Case action and the 5 most recent Cases, Accounts, and Contacts.' - ), - filenamePrefix: 'landing_page_case_management' - }, - { - label: l10n.t('Healthcare'), - detail: l10n.t( - 'Global quick actions with BarcodeScanner, new Visitor, and more.' - ), - filenamePrefix: 'landing_page_healthcare' - }, - { - label: l10n.t('Retail Execution'), - detail: l10n.t( - 'Global quick actions with new Opportunity, new Lead, and more.' - ), - filenamePrefix: 'landing_page_retail_execution' - } - ]; - - public static async chooseTemplate() { - const selectedItem = await UIUtils.showQuickPick( - l10n.t('Select a template...'), - undefined, - () => { - return new Promise(async (resolve, reject) => { - resolve(TemplateChooserCommand.TEMPLATE_LIST_ITEMS); - }); - } - ); - - if (!selectedItem) { - return Promise.resolve(); - } + 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'; + static readonly LANDING_PAGE_FILENAME_PREFIXES: { + [landingPageType in LandingPageType]: string; + } = { + existing: this.LANDING_PAGE_FILENAME_PREFIX, + default: `${this.LANDING_PAGE_FILENAME_PREFIX}_default`, + caseManagement: `${this.LANDING_PAGE_FILENAME_PREFIX}_case_management`, + healthcare: `${this.LANDING_PAGE_FILENAME_PREFIX}_healthcare`, + retail: `${this.LANDING_PAGE_FILENAME_PREFIX}_retail_execution` + }; - await TemplateChooserCommand.copySelectedFiles( - (selectedItem as TemplateQuickPickItem).filenamePrefix - ); - } - - public static async copyDefaultTemplate(extensionUri: Uri) { - await TemplateChooserCommand.copySelectedFiles( - TemplateChooserCommand.TEMPLATE_LIST_ITEMS[0].filenamePrefix - ); - - await InstructionsWebviewProvider.showDismissableInstructions( - extensionUri, - l10n.t('Landing Page Customization'), - 'resources/instructions/landingpage.html' - ); + public static async chooseTemplate(extensionUri: Uri) { + return new Promise((resolve) => { + new InstructionsWebviewProvider( + extensionUri + ).showInstructionWebview( + l10n.t('Offline Starter Kit: Select Landing Page'), + 'resources/instructions/landingPageTemplateChoice.html', + [ + { + type: 'landingPageChosen', + action: async (panel, data) => { + const landingPageChosenData = data as { + landingPageType: LandingPageType; + }; + const completed = await this.onLandingPageChosen( + landingPageChosenData + ); + if (completed) { + panel.dispose(); + return resolve(); + } + } + }, + { + type: 'landingPageStatus', + action: async (_panel, _data, callback) => { + if (callback) { + const landingPageStatus = + await this.getLandingPageStatus(); + callback(landingPageStatus); + } + } + } + ] + ); + }); } /** - * This will copy the given template files over to the staticresources/landing_page.* locations, including - * the .json and .resource-meta.xml file. - * @param fileNamePrefix filename prefix of the template file to copy. + * This will copy the chosen template files to landing_page.json and + * landing_page.resource-meta.xml in the staticresources folder of the project. + * @param choiceData The data object containing the landing page type the user + * selected. */ - static async copySelectedFiles(fileNamePrefix: string): Promise { - const workspaceFolders = workspace.workspaceFolders; - if (workspaceFolders && workspaceFolders.length > 0) { - const rootPath = workspaceFolders[0].uri.fsPath; + static async onLandingPageChosen(choiceData: { + landingPageType: LandingPageType; + }): Promise { + return new Promise(async (resolve) => { + const landingPageType = choiceData.landingPageType; + + // Nothing to do if the user chose to keep their existing landing page. + if (landingPageType === 'existing') { + return resolve(true); + } + + // If a landing page exists, warn about overwriting it. + const staticResourcesPath = await this.getStaticResourcesDir(); + const existingLandingPageFiles = await this.landingPageFilesExist( + staticResourcesPath, + 'existing' + ); + if ( + existingLandingPageFiles.jsonFileExists || + existingLandingPageFiles.metaFileExists + ) { + const confirmOverwrite = + await this.askUserToOverwriteLandingPage(); + if (confirmOverwrite === l10n.t('No')) { + console.info( + 'User chose not to overwrite their existing landing page.' + ); + return resolve(false); + } + } - // copy both the json and metadata files. + // Copy both the json and metadata files. + const sourceFilenamePrefix = + this.LANDING_PAGE_FILENAME_PREFIXES[landingPageType]; + const destFilenamePrefix = + this.LANDING_PAGE_FILENAME_PREFIXES.existing; for (const fileExtension of [ - TemplateChooserCommand.JSON_FILE_EXTENSION, - TemplateChooserCommand.METADATA_FILE_EXTENSION + this.LANDING_PAGE_JSON_FILE_EXTENSION, + this.LANDING_PAGE_METADATA_FILE_EXTENSION ]) { - const fileName = `${fileNamePrefix}${fileExtension}`; - const destinationFileName = `${TemplateChooserCommand.LANDING_PAGE_DESTINATION_FILENAME_PREFIX}${fileExtension}`; - console.log(`Copying ${fileName} to ${destinationFileName}`); - + const sourceFilename = sourceFilenamePrefix + fileExtension; + const destFilename = destFilenamePrefix + fileExtension; const sourcePath = path.join( - rootPath, - TemplateChooserCommand.STATIC_RESOURCES_PATH, - fileName + staticResourcesPath, + sourceFilename ); const destinationPath = path.join( - rootPath, - TemplateChooserCommand.STATIC_RESOURCES_PATH, - destinationFileName + staticResourcesPath, + destFilename ); - fs.copyFileSync(sourcePath, destinationPath); + await window.withProgress( + { + location: ProgressLocation.Notification, + title: l10n.t( + "Copying '{0}' to '{1}'", + sourceFilename, + destFilename + ) + }, + async (_progress, _token) => { + await copyFile(sourcePath, destinationPath); + } + ); } - return Promise.resolve(true); + return resolve(true); + }); + } + + static askUserToOverwriteLandingPage(): Thenable { + return window.showWarningMessage( + l10n.t( + 'Are you sure you want to overwrite your existing landing page?' + ), + { modal: true }, + l10n.t('Yes'), + l10n.t('No') + ); + } + + static async getLandingPageStatus(): Promise { + return new Promise(async (resolve) => { + const landingPageCollectionStatus: LandingPageCollectionStatus = { + landingPageCollection: {} + }; + + let staticResourcesPath: string; + try { + staticResourcesPath = await this.getStaticResourcesDir(); + } catch (err) { + landingPageCollectionStatus.error = (err as Error).message; + return resolve(landingPageCollectionStatus); + } + + for (const lptIndex in this.LANDING_PAGE_FILENAME_PREFIXES) { + const landingPageType = lptIndex as LandingPageType; + const landingPageFilesExist = await this.landingPageFilesExist( + staticResourcesPath, + landingPageType + ); + const landingPageExists = + landingPageFilesExist.jsonFileExists && + landingPageFilesExist.metaFileExists; + let warningMessage: string | undefined; + if ( + !landingPageFilesExist.jsonFileExists && + !landingPageFilesExist.metaFileExists + ) { + warningMessage = l10n.t( + "The landing page files '{0}{1}' and '{0}{2}' do not exist.", + this.LANDING_PAGE_FILENAME_PREFIXES[landingPageType], + this.LANDING_PAGE_JSON_FILE_EXTENSION, + this.LANDING_PAGE_METADATA_FILE_EXTENSION + ); + } else if (!landingPageFilesExist.metaFileExists) { + warningMessage = l10n.t( + "The landing page file '{0}{1}' does not exist", + this.LANDING_PAGE_FILENAME_PREFIXES[landingPageType], + this.LANDING_PAGE_METADATA_FILE_EXTENSION + ); + } else if (!landingPageFilesExist.jsonFileExists) { + warningMessage = l10n.t( + "The landing page file '{0}{1}' does not exist", + this.LANDING_PAGE_FILENAME_PREFIXES[landingPageType], + this.LANDING_PAGE_JSON_FILE_EXTENSION + ); + } + landingPageCollectionStatus.landingPageCollection[ + landingPageType + ] = { exists: landingPageExists, warning: warningMessage }; + } + return resolve(landingPageCollectionStatus); + }); + } + + static getWorkspaceDir(): string { + const workspaceFolders = workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + throw new NoWorkspaceError( + 'No workspace defined for this project.' + ); } - return Promise.reject('Could not determine workspace folder.'); + 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 + ): Promise<{ jsonFileExists: boolean; metaFileExists: boolean }> { + return new Promise<{ + jsonFileExists: boolean; + metaFileExists: boolean; + }>(async (resolve) => { + let jsonFileExists = true; + const jsonFilename = + this.LANDING_PAGE_FILENAME_PREFIXES[landingPageType] + + this.LANDING_PAGE_JSON_FILE_EXTENSION; + const jsonFilePath = path.join(staticResourcesPath, jsonFilename); + try { + await access(jsonFilePath); + } catch (err) { + console.warn( + `File '${jsonFilename}' does not exist at '${staticResourcesPath}'.` + ); + jsonFileExists = false; + } + let metaFileExists = true; + const metaFilename = + this.LANDING_PAGE_FILENAME_PREFIXES[landingPageType] + + this.LANDING_PAGE_METADATA_FILE_EXTENSION; + const metaFilePath = path.join(staticResourcesPath, metaFilename); + try { + await access(metaFilePath); + } catch (err) { + console.warn( + `File '${metaFilename}' does not exist at '${staticResourcesPath}'.` + ); + metaFileExists = false; + } + + return resolve({ jsonFileExists, metaFileExists }); + }); + } +} + +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); } } diff --git a/src/test/TestHelper.ts b/src/test/TestHelper.ts index 222f04e6..7ea8d73c 100644 --- a/src/test/TestHelper.ts +++ b/src/test/TestHelper.ts @@ -33,7 +33,8 @@ export class TempProjectDirManager { `Project dir '${this.projectDir}' is not a directory.` ); } - return rm(this.projectDir, { recursive: true, force: true }); + await rm(this.projectDir, { recursive: true, force: true }); + return resolve(); }); } diff --git a/src/test/suite/commands/wizard/landingPageCommand.test.ts b/src/test/suite/commands/wizard/landingPageCommand.test.ts index 8ac5df4a..96b73606 100644 --- a/src/test/suite/commands/wizard/landingPageCommand.test.ts +++ b/src/test/suite/commands/wizard/landingPageCommand.test.ts @@ -108,7 +108,7 @@ suite('Landing Page Command Test Suite', () => { }; UIUtils.showQuickPick = mockUIUtilsShowQuickPick; - // set up the sobject and 3 field pickers + // Set up the sObject and 3 field pickers const orgUtilsStubSobjects = sinon.stub(OrgUtils, 'getSobjects'); const sobjects = [sobject]; orgUtilsStubSobjects.returns(Promise.resolve(sobjects)); diff --git a/src/test/suite/commands/wizard/templateChooserCommand.test.ts b/src/test/suite/commands/wizard/templateChooserCommand.test.ts index 12cb516d..9e29403f 100644 --- a/src/test/suite/commands/wizard/templateChooserCommand.test.ts +++ b/src/test/suite/commands/wizard/templateChooserCommand.test.ts @@ -8,15 +8,22 @@ 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 { workspace, Uri } from 'vscode'; -import { SinonStub } from 'sinon'; import { afterEach, beforeEach } from 'mocha'; -import { UIUtils } from '../../../../utils/uiUtils'; import { TemplateChooserCommand, - TemplateQuickPickItem + NoWorkspaceError, + NoStaticResourcesDirError, + LandingPageType } from '../../../../commands/wizard/templateChooserCommand'; +import { TempProjectDirManager } from '../../../TestHelper'; + +type LandingPageTestIOConfig = { + [landingPageType in LandingPageType]?: { + [exists in 'jsonExists' | 'metaExists']: boolean; + }; +}; suite('Template Chooser Command Test Suite', () => { beforeEach(function () {}); @@ -25,68 +32,397 @@ suite('Template Chooser Command Test Suite', () => { sinon.restore(); }); - test('Selects a template file and it is copied', async () => { - const showQuickPickStub: SinonStub = sinon.stub( - UIUtils, - 'showQuickPick' - ); - - // set up file picker - const chosenItem: TemplateQuickPickItem = { - label: 'Case Management', - description: 'This is the description', - detail: 'Contains a new case quick action, along with the 5 most recent cases, accounts, and contacts.', - filenamePrefix: 'somefile' - }; - - showQuickPickStub.onCall(0).returns(chosenItem); - - // set up stubs for filesystem copy - const testPath = '/somepath'; - const copyFileSyncStub = sinon.stub(fs, 'copyFileSync'); - const workspaceFoldersStub = sinon - .stub(workspace, 'workspaceFolders') - .get(() => [{ uri: Uri.file(testPath) }]); - - // execute our command - await TemplateChooserCommand.chooseTemplate(); - - // ensure copy was performed for both json and metadata files - for (const fileExtension of [ - TemplateChooserCommand.JSON_FILE_EXTENSION, - TemplateChooserCommand.METADATA_FILE_EXTENSION - ]) { - const expectedSourcePath = path.join( - testPath, - TemplateChooserCommand.STATIC_RESOURCES_PATH, - `somefile${fileExtension}` - ); - const expectedDestinationPath = path.join( - testPath, - TemplateChooserCommand.STATIC_RESOURCES_PATH, - `${TemplateChooserCommand.LANDING_PAGE_DESTINATION_FILENAME_PREFIX}${fileExtension}` + 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( - copyFileSyncStub.calledWith( - expectedSourcePath, - expectedDestinationPath - ), - `Should attempt to copy ${expectedSourcePath} to ${expectedDestinationPath}` + 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('Nothing is selected', async () => { - const showQuickPickStub: SinonStub = sinon.stub( - UIUtils, - 'showQuickPick' + test('Landing pages exist: existing landing page file combinations', 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 }); - showQuickPickStub.onCall(0).returns(undefined); + (async () => { + for (const lptIndex in TemplateChooserCommand.LANDING_PAGE_FILENAME_PREFIXES) { + const landingPageType = lptIndex as LandingPageType; + const fileConfigList: LandingPageTestIOConfig[] = [ + { + [landingPageType]: { + jsonExists: false, + metaExists: false + } + }, + { + [landingPageType]: { + jsonExists: true, + metaExists: false + } + }, + { + [landingPageType]: { + jsonExists: false, + metaExists: true + } + }, + { + [landingPageType]: { + jsonExists: true, + metaExists: true + } + } + ]; + for (const fileConfig of fileConfigList) { + createLandingPageContent( + fileConfig, + staticResourcesAbsPath + ); + const filesExist = + await TemplateChooserCommand.landingPageFilesExist( + staticResourcesAbsPath, + landingPageType + ); + assert.equal( + filesExist.jsonFileExists, + fileConfig[landingPageType]!.jsonExists + ); + assert.equal( + filesExist.metaFileExists, + fileConfig[landingPageType]!.metaExists + ); + deleteLandingPageContent( + fileConfig, + staticResourcesAbsPath + ); + } + } + })().then(async () => { + await projectDirMgr.removeDir(); + getWorkspaceDirStub.restore(); + }); + }); - // execute our command and get the promise to ensure expected value is received. - let promise = TemplateChooserCommand.chooseTemplate(); - let result = await promise; - assert.equal(result, undefined); + test('Choosing existing landing page automatically resolves', async () => { + const choiceData: { landingPageType: LandingPageType } = { + landingPageType: 'existing' + }; + assert.ok(await TemplateChooserCommand.onLandingPageChosen(choiceData)); + }); + + test('User is asked to overwrite existing landing page', 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 }); + const config: LandingPageTestIOConfig = { + existing: { + jsonExists: true, + metaExists: false + } + }; + createLandingPageContent(config, staticResourcesAbsPath); + + const askUserToOverwriteStub = sinon.stub( + TemplateChooserCommand, + 'askUserToOverwriteLandingPage' + ); + askUserToOverwriteStub.returns( + new Promise((resolve) => { + return resolve('No'); + }) + ); + const choiceData: { landingPageType: LandingPageType } = { + landingPageType: 'caseManagement' + }; + const pageChosen = + await TemplateChooserCommand.onLandingPageChosen(choiceData); + assert.ok( + askUserToOverwriteStub.called, + 'User should have been asked if they wanted to overwrite the existing landing page.' + ); + assert.equal( + pageChosen, + false, + 'Choice was not to overwrite existing page.' + ); + + askUserToOverwriteStub.restore(); + await projectDirMgr.removeDir(); + getWorkspaceDirStub.restore(); + }); + + test('Landing page template written to landing page files', 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 }); + + const landingPageIoConfig: LandingPageTestIOConfig = { + default: { + jsonExists: true, + metaExists: true + }, + caseManagement: { + jsonExists: true, + metaExists: true + }, + healthcare: { + jsonExists: true, + metaExists: true + }, + retail: { + jsonExists: true, + metaExists: true + } + }; + createLandingPageContent(landingPageIoConfig, staticResourcesAbsPath); + + (async () => { + for (const lptIndex in TemplateChooserCommand.LANDING_PAGE_FILENAME_PREFIXES) { + const landingPageType = lptIndex as LandingPageType; + if (landingPageType === 'existing') { + continue; + } + const copied = await TemplateChooserCommand.onLandingPageChosen( + { landingPageType } + ); + assert.ok( + copied, + `Landing page for type '${landingPageType}' should have been copied.` + ); + for (const landingPageExtension of [ + TemplateChooserCommand.LANDING_PAGE_JSON_FILE_EXTENSION, + TemplateChooserCommand.LANDING_PAGE_METADATA_FILE_EXTENSION + ]) { + const fileName = + TemplateChooserCommand.LANDING_PAGE_FILENAME_PREFIXES + .existing + landingPageExtension; + const readContent = fs.readFileSync( + path.join(staticResourcesAbsPath, fileName), + { encoding: 'utf-8' } + ); + assert.equal( + readContent, + `${landingPageType} ${landingPageExtension} content` + ); + } + } + })().then(async () => { + await projectDirMgr.removeDir(); + getWorkspaceDirStub.restore(); + }); + }); + + test('Landing page status: staticresources does not exist', async () => { + const status = await TemplateChooserCommand.getLandingPageStatus(); + assert.ok(status.error && status.error.length > 0); + }); + + test('Landing page status: various file existence scenarios', 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 }); + const landingPageConfig: LandingPageTestIOConfig = { + existing: { + jsonExists: false, + metaExists: false + }, + default: { + jsonExists: true, + metaExists: false + }, + caseManagement: { + jsonExists: false, + metaExists: true + }, + healthcare: { + jsonExists: true, + metaExists: true + }, + retail: { + jsonExists: false, + metaExists: false + } + }; + + createLandingPageContent(landingPageConfig, staticResourcesAbsPath); + + const landingPageStatus = + await TemplateChooserCommand.getLandingPageStatus(); + for (const lptIndex in landingPageConfig) { + const landingPageType = lptIndex as LandingPageType; + const config = landingPageConfig[landingPageType]!; + const landingPageCollectionStatus = + landingPageStatus.landingPageCollection[landingPageType]!; + if (config.jsonExists && config.metaExists) { + assert.equal(landingPageCollectionStatus.exists, true); + assert.ok(!landingPageCollectionStatus.warning); + } else { + assert.equal(landingPageCollectionStatus.exists, false); + assert.ok( + landingPageCollectionStatus.warning && + landingPageCollectionStatus.warning.length > 0 + ); + } + } + + await projectDirMgr.removeDir(); + getWorkspaceDirStub.restore(); }); }); + +function writeLandingPageFile( + landingPageType: LandingPageType, + landingPageExtension: string, + staticResourcesPath: string +) { + const fileContent = `${landingPageType} ${landingPageExtension} content`; + const fileName = + TemplateChooserCommand.LANDING_PAGE_FILENAME_PREFIXES[landingPageType] + + landingPageExtension; + fs.writeFileSync(path.join(staticResourcesPath, fileName), fileContent); +} + +function createLandingPageContent( + landingPageIOConfig: LandingPageTestIOConfig, + staticResourcesPath: string +) { + for (const lptIndex in landingPageIOConfig) { + const landingPageType = lptIndex as LandingPageType; + if (landingPageIOConfig[landingPageType]!.jsonExists) { + writeLandingPageFile( + landingPageType, + TemplateChooserCommand.LANDING_PAGE_JSON_FILE_EXTENSION, + staticResourcesPath + ); + } + if (landingPageIOConfig[landingPageType]!.metaExists) { + writeLandingPageFile( + landingPageType, + TemplateChooserCommand.LANDING_PAGE_METADATA_FILE_EXTENSION, + staticResourcesPath + ); + } + } +} + +function deleteLandingPageFile( + landingPageType: LandingPageType, + landingPageExtension: string, + staticResourcesPath: string +) { + const fileName = + TemplateChooserCommand.LANDING_PAGE_FILENAME_PREFIXES[landingPageType] + + landingPageExtension; + fs.rmSync(path.join(staticResourcesPath, fileName)); +} + +function deleteLandingPageContent( + landingPageIOConfig: LandingPageTestIOConfig, + staticResourcesPath: string +) { + for (const lptIndex in landingPageIOConfig) { + const landingPageType = lptIndex as LandingPageType; + if (landingPageIOConfig[landingPageType]!.jsonExists) { + deleteLandingPageFile( + landingPageType, + TemplateChooserCommand.LANDING_PAGE_JSON_FILE_EXTENSION, + staticResourcesPath + ); + } + if (landingPageIOConfig[landingPageType]!.metaExists) { + deleteLandingPageFile( + landingPageType, + TemplateChooserCommand.LANDING_PAGE_METADATA_FILE_EXTENSION, + staticResourcesPath + ); + } + } +} diff --git a/src/test/suite/utils/orgUtils.test.ts b/src/test/suite/utils/orgUtils.test.ts index 496ee00b..1ba29689 100644 --- a/src/test/suite/utils/orgUtils.test.ts +++ b/src/test/suite/utils/orgUtils.test.ts @@ -140,7 +140,7 @@ suite('Org Utils Test Suite', () => { assert.equal(sobject.labelPlural, 'Labels'); }); - test('Returns list of fields for given sobject', async () => { + test('Returns list of fields for given sObject', async () => { const sobjectFields: FieldType[] = [ buildField('City', 'string', 'Label'), buildField('Name', 'string', 'ObjectName') diff --git a/src/test/suite/webviews.test.ts b/src/test/suite/webviews.test.ts index 9fc04828..c7d420fe 100644 --- a/src/test/suite/webviews.test.ts +++ b/src/test/suite/webviews.test.ts @@ -7,16 +7,16 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { Uri, WebviewPanel, env } from 'vscode'; +import { Uri, env } from 'vscode'; import { afterEach, beforeEach } from 'mocha'; import * as fs from 'node:fs'; -import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; +import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { tmpdir } from 'node:os'; import { WebviewMessageHandler, WebviewProcessor } from '../../webviews/processor'; +import { TempProjectDirManager } from '../TestHelper'; suite('Webview Test Suite', () => { const extensionUri = Uri.parse('file:///tmp/testdir'); @@ -260,10 +260,9 @@ suite('Webview Test Suite', () => { }); test('Get webview content with script demarcator', async () => { - const extensionUriTempDir = await mkdtemp( - join(tmpdir(), 'salesforcedx-vscode-mobile-') - ); - const extensionUri = Uri.file(extensionUriTempDir); + const extensionUriTempDir = + await TempProjectDirManager.createTempProjectDir(); + const extensionUri = Uri.file(extensionUriTempDir.projectDir); const processor = new WebviewProcessor(extensionUri); const webviewPanel = processor.createWebviewPanel( 'someViewType', @@ -275,10 +274,12 @@ suite('Webview Test Suite', () => { processor.getMessagingJsPathUri() )}">`; + // Absolute path vars are for the test writing content. Relative path vars are + // for processor.getWebviewContent(). const contentFilename = 'contentFile.html'; const contentDirPathRelative = 'content'; const contentDirPathAbsolute = join( - extensionUriTempDir, + extensionUriTempDir.projectDir, contentDirPathRelative ); const contentPathRelative = join( @@ -289,7 +290,10 @@ suite('Webview Test Suite', () => { contentDirPathAbsolute, contentFilename ); - await mkdir(join(extensionUriTempDir, contentDirPathRelative)); + + await mkdir( + join(extensionUriTempDir.projectDir, contentDirPathRelative) + ); await writeFile(contentPathAbsolute, contentWithDemarcator); const generatedWebviewContent = processor.getWebviewContent( @@ -300,6 +304,8 @@ suite('Webview Test Suite', () => { generatedWebviewContent, contentWithDemarcatorDereferenced ); + + await extensionUriTempDir.removeDir(); webviewPanel.dispose(); }); }); diff --git a/src/webviews/processor.ts b/src/webviews/processor.ts index 48367759..417a624f 100644 --- a/src/webviews/processor.ts +++ b/src/webviews/processor.ts @@ -71,12 +71,12 @@ export class WebviewProcessor { if (data.callbackId) { const returnedCallbackId = data.callbackId; delete data.callbackId; - callback = (responseData?: object) => { + callback = async (responseData?: object) => { const fullResponseMessage = { callbackId: returnedCallbackId, ...responseData }; - panel.webview.postMessage(fullResponseMessage); + await panel.webview.postMessage(fullResponseMessage); }; } responsiveHandler.action(panel, data, callback);