diff --git a/generators/add-vscode-config/VsCodeConfiguration.js b/generators/add-vscode-config/VsCodeConfiguration.js new file mode 100644 index 00000000..6aa2ec13 --- /dev/null +++ b/generators/add-vscode-config/VsCodeConfiguration.js @@ -0,0 +1,120 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const path = require('path') + +/** + * Create a VS Code launch compound. + * + * @param {Object} params the parameters + * @param {String} params.name the compound name + * @param {Array} params.configurations an array of launch configuration names + */ +function createLaunchCompound (params) { + const { name, configurations = [] } = params + return { + name, + configurations + } +} + +/** + * Create a VS Code basic launch configuration. + * + * @param {Object} params the parameters + * @param {String} params.type the launch configuration type + * @param {String} params.name the launch configuration name + * @param {String} params.request the launch configuration request + */ +function createLaunchConfiguration (params) { + const { type, name, request } = params + return { + type, + name, + request + } +} + +/** + * Create a VS Code Google Chrome launch configuration. + * + * This configuration needs the Chrome Debugging extension for VS Code (created by Microsoft) to be installed. + * + * @param {Object} params the parameters + * @param {String} params.url the frontend URL + * @param {String} params.webRoot the path to the web root + * @param {String} params.webDistDev the path to the web dist-dev folder + */ +function createChromeLaunchConfiguration (params) { + const { url, webRoot, webDistDev } = params + return { + ...createLaunchConfiguration({ type: 'chrome', name: 'Web', request: 'launch' }), + url, + webRoot, + breakOnLoad: true, + sourceMapPathOverrides: { + '*': path.join(webDistDev, '*') + } + } +} + +/** + * Create a VS Code Node launch configuration. + * + * @param {Object} params the parameters + * @param {String} params.packageName the Openwhisk package name + * @param {String} params.actionName the Openwhisk action name + * @param {String} params.actionFileRelativePath the relative path to the action file + * @param {String} params.envFileRelativePath the relative path to the env file + * @param {String} params.remoteRoot the remote root path + */ +function createPwaNodeLaunchConfiguration (params) { + const { packageName, actionName, actionFileRelativePath, envFileRelativePath, remoteRoot } = params + const configurationName = `Action:${packageName}/${actionName}` + + return { + ...createLaunchConfiguration({ type: 'pwa-node', name: configurationName, request: 'launch' }), + runtimeExecutable: '${workspaceFolder}/node_modules/.bin/wskdebug', // eslint-disable-line no-template-curly-in-string + envFile: `\${workspaceFolder}/${envFileRelativePath}`, + timeout: 30000, + localRoot: '${workspaceFolder}', // eslint-disable-line no-template-curly-in-string + remoteRoot, + outputCapture: 'std', + attachSimplePort: 0, + runtimeArgs: [ + `${packageName}/${actionName}`, + `\${workspaceFolder}/${actionFileRelativePath}`, + '-v' + ] + } +} + +/** + * Create a VS Code configuration. + * + * @param {Object} params the parameters + * @param {Array} params.configurations an array of VS Code launch configurations + * @param {Array} params.compunds an array of VS Code launch compounds + */ +function createVsCodeConfiguration (params = {}) { + const { configurations = [], compounds = [] } = params + return { + configurations, + compounds + } +} + +module.exports = { + createVsCodeConfiguration, + createLaunchCompound, + createChromeLaunchConfiguration, + createPwaNodeLaunchConfiguration +} diff --git a/generators/add-vscode-config/index.js b/generators/add-vscode-config/index.js new file mode 100644 index 00000000..188cb153 --- /dev/null +++ b/generators/add-vscode-config/index.js @@ -0,0 +1,242 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const Generator = require('yeoman-generator') +const path = require('path') +const fs = require('fs-extra') +const { absApp, objGetValue } = require('./utils') +const cloneDeep = require('lodash.clonedeep') + +const { + createVsCodeConfiguration, + createLaunchCompound, + createChromeLaunchConfiguration, + createPwaNodeLaunchConfiguration +} = require('./VsCodeConfiguration') + +/* + 'initializing', + 'prompting', + 'configuring', + 'default', + 'writing', + 'conflicts', + 'install', + 'end' +*/ + +const Default = { + DESTINATION_FILE: '.vscode/launch.json', + REMOTE_ROOT: '/code', + SKIP_PROMPT: false +} + +const Option = { + DESTINATION_FILE: 'destination-file', + FRONTEND_URL: 'frontend-url', + REMOTE_ROOT: 'remote-root', + APP_CONFIG: 'app-config', + ENV_FILE: 'env-file', + SKIP_PROMPT: 'skip-prompt' +} + +class AddVsCodeConfig extends Generator { + constructor (args, opts) { + super(args, opts) + + // options are inputs from CLI or yeoman parent generator + this.option(Option.APP_CONFIG, { type: Object }) + this.option(Option.FRONTEND_URL, { type: String }) + this.option(Option.REMOTE_ROOT, { type: String, default: Default.REMOTE_ROOT }) + this.option(Option.DESTINATION_FILE, { type: String, default: Default.DESTINATION_FILE }) + this.option(Option.ENV_FILE, { type: String }) + this.option(Option.SKIP_PROMPT, { type: Boolean, default: Default.SKIP_PROMPT }) + } + + _verifyConfig () { + const appConfig = this.options[Option.APP_CONFIG] + const verifyKeys = [ + 'app.hasFrontend', + 'app.hasBackend', + 'ow.package', + 'ow.apihost', + 'manifest.packagePlaceholder', + 'manifest.full.packages', + 'web.src', + 'web.distDev', + 'root' + ] + + const missingKeys = [] + verifyKeys.forEach(key => { + if (objGetValue(appConfig, key) === undefined) { + missingKeys.push(key) + } + }) + + if (missingKeys.length > 0) { + throw new Error(`App config missing keys: ${missingKeys.join(', ')}`) + } + + const envFile = this.options[Option.ENV_FILE] + if (!envFile) { + throw new Error(`Missing option for generator: ${Option.ENV_FILE}`) + } + } + + _getActionEntryFile (pkgJson) { + const pkgJsonContent = fs.readJsonSync(pkgJson) + if (pkgJsonContent.main) { + return pkgJsonContent.main + } + return 'index.js' + } + + _processRuntimeArgsForActionEntryFile (action, runtimeArgs) { + const appConfig = this.options[Option.APP_CONFIG] + const actionPath = absApp(appConfig.root, action.function) + + const actionFileStats = fs.lstatSync(actionPath) + if (actionFileStats.isDirectory()) { + // take package.json main or 'index.js' + const zipMain = this._getActionEntryFile(path.join(actionPath, 'package.json')) + return path.join(actionPath, zipMain) + } + + return runtimeArgs + } + + _processAction (packageName, actionName, action) { + const appConfig = this.options[Option.APP_CONFIG] + const nodeVersion = this.options[Option.NODE_VERSION] + const remoteRoot = this.options[Option.REMOTE_ROOT] + const envFile = this.options[Option.ENV_FILE] + + const launchConfig = createPwaNodeLaunchConfiguration({ + packageName, + actionName, + actionFileRelativePath: action.function, + envFileRelativePath: envFile, + remoteRoot, + nodeVersion + }) + + launchConfig.runtimeArgs = this._processRuntimeArgsForActionEntryFile(action, launchConfig.runtimeArgs) + + if ( + action.annotations && + action.annotations['require-adobe-auth'] && + appConfig.ow.apihost === 'https://adobeioruntime.net' + ) { + // NOTE: The require-adobe-auth annotation is a feature implemented in the + // runtime plugin. The current implementation replaces the action by a sequence + // and renames the action to __secured_. The annotation will soon be + // natively supported in Adobe I/O Runtime, at which point this condition won't + // be needed anymore. + /* instanbul ignore next */ + launchConfig.runtimeArgs[0] = `${packageName}/__secured_${actionName}` + } + + if (action.runtime) { + launchConfig.runtimeArgs.push('--kind') + launchConfig.runtimeArgs.push(action.runtime) + } + + return launchConfig + } + + _processForBackend () { + const appConfig = this.options[Option.APP_CONFIG] + + const modifiedConfig = cloneDeep(appConfig) + const packages = modifiedConfig.manifest.full.packages + const packagePlaceholder = modifiedConfig.manifest.packagePlaceholder + if (packages[packagePlaceholder]) { + packages[modifiedConfig.ow.package] = packages[packagePlaceholder] + delete packages[packagePlaceholder] + } + + Object.keys(packages).forEach(packageName => { + const pkg = packages[packageName] + + Object.keys(pkg.actions).forEach(actionName => { + const action = pkg.actions[actionName] + const launchConfig = this._processAction(packageName, actionName, action) + this.vsCodeConfig.configurations.push(launchConfig) + }) + }) + + this.vsCodeConfig.compounds.push({ + name: 'Actions', + configurations: this.vsCodeConfig.configurations.map(config => config.name) + }) + } + + _processForFrontend () { + const appConfig = this.options[Option.APP_CONFIG] + const frontEndUrl = this.options[Option.FRONTEND_URL] + + if (!frontEndUrl) { + throw new Error(`Missing option for generator: ${Option.FRONTEND_URL}`) + } + + const webConfig = createChromeLaunchConfiguration({ + url: frontEndUrl, + webRoot: appConfig.web.src, + webDistDev: appConfig.web.distDev + }) + + this.vsCodeConfig.configurations.push(webConfig) + + this.vsCodeConfig.compounds.push(createLaunchCompound({ + name: 'WebAndActions', + configurations: this.vsCodeConfig.configurations.map(config => config.name) + })) + } + + initializing () { + this._verifyConfig() + this.vsCodeConfig = createVsCodeConfiguration() + + const appConfig = this.options[Option.APP_CONFIG] + + if (appConfig.app.hasBackend) { + this._processForBackend() + } + + if (appConfig.app.hasFrontend) { + this._processForFrontend() + } + } + + async writing () { + const destFile = this.options[Option.DESTINATION_FILE] + const skipPrompt = this.options[Option.SKIP_PROMPT] + + let confirm = { overwriteVsCodeConfig: true } + + if (fs.existsSync(destFile) && !skipPrompt) { + confirm = await this.prompt([ + { + type: 'confirm', + name: 'overwriteVsCodeConfig', + message: `Please confirm the overwrite of your Visual Studio Code launch configuration in '${destFile}'?` + } + ]) + } + + if (confirm.overwriteVsCodeConfig) { + this.fs.writeJSON(this.destinationPath(destFile), this.vsCodeConfig) + } + } +} + +module.exports = AddVsCodeConfig diff --git a/generators/add-vscode-config/utils.js b/generators/add-vscode-config/utils.js new file mode 100644 index 00000000..76215232 --- /dev/null +++ b/generators/add-vscode-config/utils.js @@ -0,0 +1,31 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const path = require('path') + +function absApp (root, p) { + if (path.isAbsolute(p)) return p + return path.join(root, path.normalize(p)) +} + +function objGetProp (obj, key) { + return obj[Object.keys(obj).find(k => k.toLowerCase() === key.toLowerCase())] +} + +function objGetValue (obj, key) { + const keys = (key || '').toString().split('.') + return keys.filter(o => o.trim()).reduce((o, i) => o && objGetProp(o, i), obj) +} + +module.exports = { + absApp, + objGetValue +} diff --git a/package.json b/package.json index a7ea968d..a8aca217 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "description": "Adobe I/O application yeoman code generator", "main": "generators/app/index.js", "scripts": { - "test": "eslint . && jest -c ./jest.config.js", - "unit-test": "jest -c ./jest.config.js" + "lint": "eslint .", + "test": "npm run lint && npm run unit-tests", + "unit-tests": "jest -c ./jest.config.js" }, "repository": { "type": "git", @@ -44,6 +45,7 @@ "dependencies": { "fs-extra": "^9.0.0", "js-yaml": "^3.14.0", + "lodash.clonedeep": "^4.5.0", "upath": "^1.2.0", "yeoman-generator": "^4.10.1" } diff --git a/test/generators/add-vscode-config/VsCodeConfiguration.test.js b/test/generators/add-vscode-config/VsCodeConfiguration.test.js new file mode 100644 index 00000000..f7326406 --- /dev/null +++ b/test/generators/add-vscode-config/VsCodeConfiguration.test.js @@ -0,0 +1,98 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +const { + createVsCodeConfiguration, + createLaunchCompound, + createChromeLaunchConfiguration, + createPwaNodeLaunchConfiguration +} = require('../../../generators/add-vscode-config/VsCodeConfiguration') + +const path = require('path') + +test('exports', () => { + expect(typeof createVsCodeConfiguration).toEqual('function') + expect(typeof createLaunchCompound).toEqual('function') + expect(typeof createChromeLaunchConfiguration).toEqual('function') + expect(typeof createPwaNodeLaunchConfiguration).toEqual('function') +}) + +test('createVsCodeConfiguration', () => { + const launchConfig = createVsCodeConfiguration() + + expect(typeof launchConfig).toEqual('object') + expect(Array.isArray(launchConfig.configurations)).toBeTruthy() + expect(Array.isArray(launchConfig.compounds)).toBeTruthy() +}) + +test('createLaunchCompound', () => { + const compoundName = 'compound-name' + const launchCompound = createLaunchCompound({ name: compoundName }) + + expect(typeof launchCompound).toEqual('object') + expect(launchCompound.name).toEqual(compoundName) + expect(Array.isArray(launchCompound.configurations)).toBeTruthy() +}) + +test('createChromeLaunchConfiguration', () => { + const params = { + url: 'my-url', + webRoot: 'my-web-root', + webDistDev: 'dist-dev' + } + const launchConfig = createChromeLaunchConfiguration(params) + + expect(typeof launchConfig).toEqual('object') + expect(launchConfig.type).toEqual('chrome') + expect(launchConfig.name).toEqual('Web') + expect(launchConfig.request).toEqual('launch') + + expect(launchConfig).toMatchObject({ + type: 'chrome', + name: 'Web', + request: 'launch', + url: params.url, + webRoot: params.webRoot, + breakOnLoad: true, + sourceMapPathOverrides: { + '*': path.join(params.webDistDev, '*') + } + }) +}) + +test('createPwaNodeLaunchConfiguration', () => { + const params = { + packageName: 'my-package', + actionName: 'my-action-name', + actionFileRelativePath: 'action-relative-path', + envFileRelativePath: 'env-file-relative-path', + remoteRoot: 'remote-root', + nodeVersion: 14 + } + const launchConfig = createPwaNodeLaunchConfiguration(params) + + expect(launchConfig).toMatchObject({ + type: 'pwa-node', + name: `Action:${params.packageName}/${params.actionName}`, + request: 'launch', + runtimeExecutable: '${workspaceFolder}/node_modules/.bin/wskdebug', // eslint-disable-line no-template-curly-in-string + envFile: '${workspaceFolder}/env-file-relative-path', // eslint-disable-line no-template-curly-in-string + timeout: 30000, + localRoot: '${workspaceFolder}', // eslint-disable-line no-template-curly-in-string + remoteRoot: params.remoteRoot, + outputCapture: 'std', + attachSimplePort: 0, + runtimeArgs: [ + `${params.packageName}/${params.actionName}`, + `\${workspaceFolder}/${params.actionFileRelativePath}`, // eslint-disable-line no-template-curly-in-string + '-v' + ] + }) +}) diff --git a/test/generators/add-vscode-config/index.test.js b/test/generators/add-vscode-config/index.test.js new file mode 100644 index 00000000..257efabd --- /dev/null +++ b/test/generators/add-vscode-config/index.test.js @@ -0,0 +1,273 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +const helpers = require('yeoman-test') +const assert = require('yeoman-assert') +const fs = require('fs-extra') +const path = require('path') + +jest.mock('fs-extra') + +const theGeneratorPath = require.resolve('../../../generators/add-vscode-config') +const Generator = require('yeoman-generator') + +const createOptions = () => { + return { + 'app-config': { + app: { + hasBackend: true, + hasFrontend: true + }, + ow: { + package: 'my-package', + apihost: 'https://my-api.host' + }, + manifest: { + packagePlaceholder: '__APP_PACKAGE__', + full: { + packages: { + __APP_PACKAGE__: { + actions: { + 'action-1': { function: 'src/actions/action-1' } + } + } + } + } + }, + web: { + src: 'html', + distDev: 'dist-dev' + }, + root: 'root' + }, + 'frontend-url': 'https://localhost:9080', + 'env-file': 'my/.env' + } +} + +const createTestLaunchConfiguration = (packageName) => { + return { + configurations: [ + { + type: 'pwa-node', + name: `Action:${packageName}/action-1`, + request: 'launch', + runtimeExecutable: '${workspaceFolder}/node_modules/.bin/wskdebug', // eslint-disable-line no-template-curly-in-string + envFile: '${workspaceFolder}/my/.env', // eslint-disable-line no-template-curly-in-string + timeout: 30000, + localRoot: '${workspaceFolder}', // eslint-disable-line no-template-curly-in-string + remoteRoot: '/code', + outputCapture: 'std', + attachSimplePort: 0, + runtimeArgs: [ + `${packageName}/__secured_action-1`, + '${workspaceFolder}/src/actions/action-1', // eslint-disable-line no-template-curly-in-string + '-v', + '--kind', + 'nodejs:14' + ] + }, + { + type: 'chrome', + name: 'Web', + request: 'launch', + url: 'https://localhost:9080', + webRoot: 'html', + breakOnLoad: true, + sourceMapPathOverrides: { + '*': path.join('dist-dev', '*') + } + } + ], + compounds: [ + { + name: 'Actions', + configurations: [ + `Action:${packageName}/action-1` + ] + }, + { + name: 'WebAndActions', + configurations: [ + `Action:${packageName}/action-1`, + 'Web' + ] + } + ] + } +} + +beforeEach(() => { + fs.lstatSync.mockReset() + fs.existsSync.mockReset() +}) + +test('exports a yeoman generator', () => { + expect(require(theGeneratorPath).prototype).toBeInstanceOf(Generator) +}) + +test('option app-config incomplete', async () => { + const options = { + 'app-config': { + app: { + } + } + } + const result = helpers.run(theGeneratorPath).withOptions(options) + + await expect(result).rejects.toEqual(new Error( + 'App config missing keys: app.hasFrontend, app.hasBackend, ow.package, ow.apihost, manifest.packagePlaceholder, manifest.full.packages, web.src, web.distDev, root')) +}) + +test('option frontend-url missing', async () => { + const options = createOptions() + options['app-config'].app.hasBackend = false + options['app-config'].app.hasFrontend = true + options['frontend-url'] = undefined + options['env-file'] = 'env-file' + + const result = helpers.run(theGeneratorPath).withOptions(options) + await expect(result).rejects.toEqual(new Error('Missing option for generator: frontend-url')) +}) + +test('option env-file missing', async () => { + const options = createOptions() + options['app-config'].app.hasBackend = true + options['app-config'].app.hasFrontend = true + options['frontend-url'] = 'https://localhost:9999' + delete options['env-file'] + + const result = helpers.run(theGeneratorPath).withOptions(options) + await expect(result).rejects.toEqual(new Error('Missing option for generator: env-file')) +}) + +test('no missing options (action is a file)', async () => { + const options = createOptions() + options['app-config'].app.hasBackend = false + options['app-config'].app.hasFrontend = false + + fs.lstatSync.mockReturnValue({ + isDirectory: () => false + }) + + const result = helpers.run(theGeneratorPath).withOptions(options) + await expect(result).resolves.not.toThrow() +}) + +test('no missing options (action is a folder)', async () => { + const options = createOptions() + let result + + fs.lstatSync.mockReturnValue({ + isDirectory: () => true + }) + + fs.readJsonSync.mockReturnValue({}) // no main property in package.json + result = helpers.run(theGeneratorPath).withOptions(options) + await expect(result).resolves.not.toThrow() + + fs.readJsonSync.mockReturnValue({ main: 'main.js' }) // has main property in package.json + result = helpers.run(theGeneratorPath).withOptions(options) + await expect(result).resolves.not.toThrow() +}) + +test('no missing options (coverage: action has a runtime specifier)', async () => { + const options = createOptions() + const pkg = options['app-config'].manifest.full.packages.__APP_PACKAGE__ + pkg.actions['action-1'].runtime = 'nodejs:14' + + fs.lstatSync.mockReturnValue({ + isDirectory: () => false + }) + + const result = helpers.run(theGeneratorPath).withOptions(options) + await expect(result).resolves.not.toThrow() +}) + +test('no missing options (coverage: action has annotations)', async () => { + const options = createOptions() + options['app-config'].ow.apihost = 'https://adobeioruntime.net' + const pkg = options['app-config'].manifest.full.packages.__APP_PACKAGE__ + pkg.actions['action-1'].annotations = { 'require-adobe-auth': true } + + fs.lstatSync.mockReturnValue({ + isDirectory: () => false + }) + + const result = helpers.run(theGeneratorPath).withOptions(options) + await expect(result).resolves.not.toThrow() +}) + +test('output check', async () => { + const options = createOptions() + const pkg = options['app-config'].manifest.full.packages.__APP_PACKAGE__ + pkg.actions['action-1'].runtime = 'nodejs:14' + pkg.actions['action-1'].annotations = { + 'require-adobe-auth': true + } + options['app-config'].ow.apihost = 'https://adobeioruntime.net' + options['destination-file'] = 'foo/bar.json' + + fs.lstatSync.mockReturnValue({ + isDirectory: () => false + }) + + const result = helpers.run(theGeneratorPath).withOptions(options) + await expect(result).resolves.not.toThrow() + + const destFile = options['destination-file'] + assert.file(destFile) // destination file is written + assert.JSONFileContent(destFile, createTestLaunchConfiguration(options['app-config'].ow.package)) +}) + +test('output check (custom package)', async () => { + const customPackage = 'my-custom-package' + const options = createOptions() + const packages = options['app-config'].manifest.full.packages + packages[customPackage] = Object.assign({}, packages.__APP_PACKAGE__) + delete packages.__APP_PACKAGE__ + packages[customPackage].actions['action-1'].runtime = 'nodejs:14' + packages[customPackage].actions['action-1'].annotations = { + 'require-adobe-auth': true + } + options['app-config'].ow.apihost = 'https://adobeioruntime.net' + options['destination-file'] = 'foo/bar.json' + + fs.lstatSync.mockReturnValue({ + isDirectory: () => false + }) + + const result = helpers.run(theGeneratorPath).withOptions(options) + await expect(result).resolves.not.toThrow() + + const destFile = options['destination-file'] + assert.file(destFile) // destination file is written + assert.JSONFileContent(destFile, createTestLaunchConfiguration(customPackage)) +}) + +test('vscode launch configuration exists', async () => { + const options = createOptions() + options['destination-file'] = 'foo/bar.json' + + fs.lstatSync.mockReturnValue({ + isDirectory: () => false + }) + + fs.existsSync.mockReturnValue(true) // destination file exists + + const result = helpers + .run(theGeneratorPath) + .withOptions(options) + .withPrompts({ overwriteVsCodeConfig: false }) + await expect(result).resolves.not.toThrow() + + const destFile = options['destination-file'] + assert.noFile(destFile) // destination file is not written +}) diff --git a/test/generators/add-vscode-config/utils.test.js b/test/generators/add-vscode-config/utils.test.js new file mode 100644 index 00000000..0697923a --- /dev/null +++ b/test/generators/add-vscode-config/utils.test.js @@ -0,0 +1,45 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const { + absApp, + objGetValue +} = require('../../../generators/add-vscode-config/utils') +const path = require('path') + +test('exports', () => { + expect(typeof absApp).toEqual('function') + expect(typeof objGetValue).toEqual('function') +}) + +test('absApp', () => { + const root = '/foo' + expect(() => absApp(undefined, undefined)).toThrowError() + expect(() => absApp(undefined, 'bar')).toThrowError() + expect(() => absApp(root, undefined)).toThrowError() + + expect(absApp(root, 'bar')).toEqual(path.join(root, 'bar')) + expect(absApp(root, path.join(root, 'bar'))).toEqual(path.join(root, 'bar')) +}) + +test('objGetValue', () => { + const obj = { + foo: { + bar: 'baz' + } + } + + expect(objGetValue(undefined, undefined)).toEqual(undefined) + expect(objGetValue(undefined, 'foo')).toEqual(undefined) + expect(objGetValue(obj, undefined)).toEqual(obj) + expect(objGetValue(obj, 'foo')).toEqual({ bar: 'baz' }) + expect(objGetValue(obj, 'foo.bar')).toEqual('baz') +})