diff --git a/bin/ui5.js b/bin/ui5.js index 847d506e..618a20cd 100755 --- a/bin/ui5.js +++ b/bin/ui5.js @@ -45,6 +45,10 @@ setTimeout(() => { shouldNotifyInNpmScript: true }).notify(); + cli.parserConfiguration({ + "parse-numbers": false + }); + // Explicitly set CLI version as the yargs default might // be wrong in case a local CLI installation is used // Also add CLI location diff --git a/lib/cli/commands/add.js b/lib/cli/commands/add.js new file mode 100644 index 00000000..5c590ace --- /dev/null +++ b/lib/cli/commands/add.js @@ -0,0 +1,88 @@ +// Add +const addCommand = { + command: "add [--development] [--optional] ", + describe: "Add SAPUI5/OpenUI5 framework libraries to the project configuration.", + middlewares: [require("../middlewares/base.js")] +}; + +addCommand.builder = function(cli) { + return cli + .positional("framework-libraries", { + describe: "Framework library names", + type: "string" + }).option("development", { + describe: "Add as development dependency", + alias: ["D", "dev"], + default: false, + type: "boolean" + }).option("optional", { + describe: "Add as optional dependency", + alias: ["O"], + default: false, + type: "boolean" + }) + .example("$0 add sap.ui.core sap.m", "Add the framework libraries sap.ui.core and sap.m as dependencies") + .example("$0 add -D sap.ui.support", "Add the framework library sap.ui.support as development dependency") + .example("$0 add --optional themelib_sap_fiori_3", + "Add the framework library themelib_sap_fiori_3 as optional dependency"); +}; + +addCommand.handler = async function(argv) { + const libraryNames = argv["framework-libraries"] || []; + const development = argv["development"]; + const optional = argv["optional"]; + + if (libraryNames.length === 0) { + // Should not happen via yargs as parameter is mandatory + throw new Error("Missing mandatory parameter framework-libraries"); + } + + if (development && optional) { + throw new Error("Options 'development' and 'optional' cannot be combined"); + } + + const normalizerOptions = { + translatorName: argv.translator, + configPath: argv.config + }; + + const libraries = libraryNames.map((name) => { + const library = {name}; + if (optional) { + library.optional = true; + } else if (development) { + library.development = true; + } + return library; + }); + + const {yamlUpdated} = await require("../../framework/add")({ + normalizerOptions, + libraries + }); + + const library = libraries.length === 1 ? "library": "libraries"; + if (!yamlUpdated) { + if (argv.config) { + throw new Error( + `Internal error while adding framework ${library} ${libraryNames.join(" ")} to config at ${argv.config}` + ); + } else { + throw new Error( + `Internal error while adding framework ${library} ${libraryNames.join(" ")} to ui5.yaml` + ); + } + } else { + console.log(`Updated configuration written to ${argv.config || "ui5.yaml"}`); + let logMessage = `Added framework ${library} ${libraryNames.join(" ")} as`; + if (development) { + logMessage += " development"; + } else if (optional) { + logMessage += " optional"; + } + logMessage += libraries.length === 1 ? " dependency": " dependencies"; + console.log(logMessage); + } +}; + +module.exports = addCommand; diff --git a/lib/cli/commands/use.js b/lib/cli/commands/use.js new file mode 100644 index 00000000..8e9b01ef --- /dev/null +++ b/lib/cli/commands/use.js @@ -0,0 +1,83 @@ +// Use +const useCommand = { + command: "use ", + describe: "Initialize or update the project's framework configuration.", + middlewares: [require("../middlewares/base.js")] +}; + +useCommand.builder = function(cli) { + return cli + .positional("framework-info", { + describe: "Framework name, version or both (name@version).\n" + + "Name can be \"SAPUI5\" or \"OpenUI5\" (case-insensitive).\n" + + "Version can be \"latest\", \"1.xx\" or \"1.xx.x\".", + type: "string" + }) + .example("$0 use sapui5@latest", "Use SAPUI5 in the latest available version") + .example("$0 use openui5@1.76", "Use OpenUI5 in the latest available 1.76 patch version") + .example("$0 use latest", "Use the latest available version of the configured framework") + .example("$0 use openui5", "Use OpenUI5 without a version (or use existing version)"); +}; + +const versionRegExp = /^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?$/; + +function parseFrameworkInfo(frameworkInfo) { + const parts = frameworkInfo.split("@"); + if (parts.length > 2) { + // More than one @ sign + throw new Error("Invalid framework info: " + frameworkInfo); + } + if (parts.length === 1) { + // No @ sign, only name or version + const nameOrVersion = parts[0]; + if (!nameOrVersion) { + throw new Error("Invalid framework info: " + frameworkInfo); + } + if (nameOrVersion === "latest" || versionRegExp.test(nameOrVersion)) { + return { + name: null, + version: nameOrVersion + }; + } else { + return { + name: nameOrVersion, + version: null + }; + } + } else { + const [name, version] = parts; + if (!name || !version) { + throw new Error("Invalid framework info: " + frameworkInfo); + } + return {name, version}; + } +} + +useCommand.handler = async function(argv) { + const frameworkOptions = parseFrameworkInfo(argv["framework-info"]); + + const normalizerOptions = { + translatorName: argv.translator, + configPath: argv.config + }; + + const {usedFramework, usedVersion, yamlUpdated} = await require("../../framework/use")({ + normalizerOptions, + frameworkOptions + }); + + if (!yamlUpdated) { + if (argv.config) { + throw new Error( + `Internal error while updating config at ${argv.config} to ${usedFramework} version ${usedVersion}` + ); + } else { + throw new Error(`Internal error while updating ui5.yaml to ${usedFramework} version ${usedVersion}`); + } + } else { + console.log(`Updated configuration written to ${argv.config || "ui5.yaml"}`); + console.log(`This project is now using ${usedFramework} version ${usedVersion}`); + } +}; + +module.exports = useCommand; diff --git a/lib/framework/add.js b/lib/framework/add.js new file mode 100644 index 00000000..e53448f8 --- /dev/null +++ b/lib/framework/add.js @@ -0,0 +1,77 @@ +const {getRootProjectConfiguration, getFrameworkResolver, isValidSpecVersion} = require("./utils"); + +module.exports = async function({normalizerOptions, libraries}) { + const project = await getRootProjectConfiguration({normalizerOptions}); + + if (!isValidSpecVersion(project.specVersion)) { + throw new Error( + `ui5 add command requires specVersion "2.0" or higher. ` + + `Project ${project.metadata.name} uses specVersion "${project.specVersion}"` + ); + } + + if (!project.framework) { + throw new Error( + `Project ${project.metadata.name} is missing a framework configuration. ` + + `Please use "ui5 use" to configure a framework and version.` + ); + } + if (!project.framework.version) { + throw new Error( + `Project ${project.metadata.name} does not define a framework version configuration. ` + + `Please use "ui5 use" to configure a version.` + ); + } + + const Resolver = getFrameworkResolver(project.framework.name); + + const resolver = new Resolver({ + cwd: project.path, + version: project.framework.version + }); + + // Get metadata of all libraries to verify that they can be installed + await Promise.all(libraries.map(async ({name}) => { + try { + await resolver.getLibraryMetadata(name); + } catch (err) { + throw new Error(`Failed to find ${project.framework.name} framework library ${name}: ` + err.message); + } + })); + + // Shallow copy of given libraries to not modify the input parameter when pushing other libraries + const allLibraries = [...libraries]; + + if (project.framework.libraries) { + project.framework.libraries.forEach((library) => { + // Don't add libraries twice! + if (allLibraries.findIndex(($) => $.name === library.name) === -1) { + allLibraries.push(library); + } + }); + } + allLibraries.sort((a, b) => { + return a.name.localeCompare(b.name); + }); + + // Try to update YAML file but still return with name and resolved version in case it failed + let yamlUpdated = false; + try { + await require("./updateYaml")({ + project, + data: { + framework: { + libraries: allLibraries + } + } + }); + yamlUpdated = true; + } catch (err) { + if (err.name !== "FrameworkUpdateYamlFailed") { + throw err; + } + } + return { + yamlUpdated + }; +}; diff --git a/lib/framework/updateYaml.js b/lib/framework/updateYaml.js new file mode 100644 index 00000000..850b3e17 --- /dev/null +++ b/lib/framework/updateYaml.js @@ -0,0 +1,250 @@ +const path = require("path"); +const log = require("@ui5/logger").getLogger("cli:framework:updateYaml"); +const {loadAll, safeDump, DEFAULT_SAFE_SCHEMA} = require("js-yaml"); +const {fromYaml, getPosition, getValue} = require("data-with-position"); + +function safeLoadAll({configFile, configPath}) { + // Using loadAll with DEFAULT_SAFE_SCHEMA instead of safeLoadAll to pass "filename". + // safeLoadAll doesn't handle its parameters properly. + // See https://github.com/nodeca/js-yaml/issues/456 and https://github.com/nodeca/js-yaml/pull/381 + return loadAll(configFile, undefined, { + filename: configPath, + schema: DEFAULT_SAFE_SCHEMA + }); +} + +function getProjectYamlDocument({project, configFile, configPath}) { + const configs = safeLoadAll({configFile, configPath}); + + const projectDocumentIndex = configs.findIndex((config) => { + return config.metadata && config.metadata.name === project.metadata.name; + }); + if (projectDocumentIndex === -1) { + throw new Error( + `Could not find project with name ${project.metadata.name} in YAML: ${configPath}` + ); + } + + const matchAll = require("string.prototype.matchall"); + const matchDocumentSeparator = /^---/gm; + const documents = matchAll(configFile, matchDocumentSeparator); + let currentDocumentIndex = 0; + let currentIndex = 0; + for (const document of documents) { + // If the first separator is not at the beginning of the file + // we are already at document index 1 + // Using String#trim() to remove any whitespace characters + if (currentDocumentIndex === 0 && configFile.substring(0, document.index).trim().length > 0) { + currentDocumentIndex = 1; + } + + if (currentDocumentIndex === projectDocumentIndex) { + currentIndex = document.index; + break; + } + + currentDocumentIndex++; + } + + return { + projectDocumentContent: configFile.substring(currentIndex), + projectDocumentStartIndex: currentIndex + }; +} + +function applyChanges(string, changes) { + function positionToIndex(position) { + // Match the n-th line-ending to find the start of the given line + const lineStartPattern = new RegExp("(?:(?:\r?\n)([^\r\n]*)){" + (position.line - 1) + "}"); + const lineStartMatch = lineStartPattern.exec(string); + if (!lineStartMatch) { + throw new Error("Could not find line start!"); + } + // Add column number -1 (as column 1 starts at index 0) + return lineStartMatch.index + lineStartMatch[0].length - lineStartMatch[1].length + position.column - 1; + } + + const indexReplacements = changes.map((change) => { + if (change.type === "update") { + return { + startIndex: positionToIndex(change.position.start), + endIndex: positionToIndex(change.position.end), + value: change.value + }; + } else if (change.type === "insert") { + return { + startIndex: positionToIndex(change.parentPosition.end) + 1, + endIndex: positionToIndex(change.parentPosition.end) + 1, + value: change.value + }; + } + }).sort((a, b) => { + // Sort decending by endIndex + // This means replacements are done from bottom to top to not affect length/index of upcoming replacements + + if (a.endIndex < b.endIndex) { + return 1; + } + if (a.endIndex > b.endIndex) { + return -1; + } + return 0; + }); + + const array = Array.from(string); + indexReplacements.forEach((indexReplacement) => { + array.splice( + /* index */ indexReplacement.startIndex, + /* count */ indexReplacement.endIndex - indexReplacement.startIndex, + /* insert */ indexReplacement.value + ); + }); + return array.join(""); +} + +function getValueFromPath(data, path) { + return path.reduce((currentData, pathSegment) => { + return currentData[pathSegment]; + }, data); +} + +function getPositionFromPath(positionData, path) { + return getPosition(getValueFromPath(positionData, path)); +} + +function formatValue(value, indent) { + if (typeof value === "string") { + // TOOD: Use better logic? + if (value.includes(".")) { + return ` "${value}"`; // Put quotes around versions + } else { + return " " + value; + } + } else if (typeof value === "object" && !Array.isArray(value)) { + let string = "\n"; + Object.keys(value).forEach((key, i, arr) => { + const entry = value[key]; + string += " ".repeat(indent) + key + ":" + formatValue(entry); + if (i < arr.length - 1) { + string += "\n"; + } + }); + return string; + } else if (Array.isArray(value)) { + const indentString = " ".repeat(indent); + const string = safeDump(value); + const arr = string.split("\n"); + arr.pop(); + return "\n" + indentString + arr.join("\n" + indentString); + } +} + +module.exports = async function({project, data}) { + const {promisify} = require("util"); + const fs = require("fs"); + const readFile = promisify(fs.readFile); + const writeFile = promisify(fs.writeFile); + + const configPath = project.configPath || path.join(project.path, "ui5.yaml"); + const configFile = await readFile(configPath, {encoding: "utf8"}); + + let { + projectDocumentContent, + projectDocumentStartIndex + } = await getProjectYamlDocument({ + project, + configFile, + configPath + }); + + const positionData = fromYaml(projectDocumentContent); + + const changes = []; + + function addInsert(entryPath, newValue) { + // New + const parentPath = entryPath.slice(0, -1); + const parentData = getValueFromPath(positionData, parentPath); + const parentPosition = getPosition(parentData); + const siblings = Object.keys(parentData); + let indent; + if (siblings.length === 0) { + indent = parentPosition.start.column - 1; + } else { + const firstSiblingPosition = getPosition(parentData[siblings[0]]); + indent = firstSiblingPosition.start.column - 1; + } + changes.push({ + type: "insert", + parentPosition, + value: `${" ".repeat(indent)}${entryPath[entryPath.length - 1]}:${formatValue(newValue, indent + 2)}\n` + }); + } + + function addUpdate(entryPath, newValue) { + const position = getPositionFromPath(positionData, entryPath); + // -1 as column 1 starts at index 0 + const indent = position.start.column - 1; + changes.push({ + type: "update", + position: getPositionFromPath(positionData, entryPath), + value: `${entryPath[entryPath.length - 1]}:${formatValue(newValue, indent + 2)}` + }); + } + + if (!positionData.framework) { + addInsert(["framework"], data.framework); + } else { + if (data.framework.name) { + if (!positionData.framework.name) { + addInsert(["framework", "name"], data.framework.name); + } else if (getValue(positionData.framework.name) !== data.framework.name) { + addUpdate(["framework", "name"], data.framework.name); + } + } + if (data.framework.version) { + if (!positionData.framework.version) { + addInsert(["framework", "version"], data.framework.version); + } else if (getValue(positionData.framework.version) !== data.framework.version) { + addUpdate(["framework", "version"], data.framework.version); + } + } + if (data.framework.libraries) { + if (!positionData.framework.libraries) { + addInsert(["framework", "libraries"], data.framework.libraries); + } else { + addUpdate(["framework", "libraries"], data.framework.libraries); + } + } + } + + // TODO: detect windows line-endings + if (!projectDocumentContent.endsWith("\n")) { + projectDocumentContent += "\n"; + } + + const adoptedProjectYaml = applyChanges(projectDocumentContent, changes); + + const array = Array.from(configFile); + array.splice(projectDocumentStartIndex, projectDocumentContent.length, adoptedProjectYaml); + let adoptedYaml = array.join(""); + + // TODO: detect windows line-endings + if (!adoptedYaml.endsWith("\n")) { + adoptedYaml += "\n"; + } + + // Validate content before writing + try { + safeLoadAll({configFile: adoptedYaml}); + } catch (err) { + const error = new Error("Failed to update YAML file: " + err.message); + error.name = "FrameworkUpdateYamlFailed"; + log.verbose(error.message); + log.verbose(`Original YAML (${configPath}):\n` + configFile); + log.verbose("Updated YAML:\n" + adoptedYaml); + throw error; + } + + await writeFile(configPath, adoptedYaml); +}; diff --git a/lib/framework/use.js b/lib/framework/use.js new file mode 100644 index 00000000..4730400c --- /dev/null +++ b/lib/framework/use.js @@ -0,0 +1,72 @@ +const {getRootProjectConfiguration, getFrameworkResolver, isValidSpecVersion} = require("./utils"); + +async function resolveVersion({frameworkName, frameworkVersion}, resolverOptions) { + return await getFrameworkResolver(frameworkName).resolveVersion(frameworkVersion, resolverOptions); +} + +function getEffectiveFrameworkName({project, frameworkOptions}) { + if (!project.framework && !frameworkOptions.name) { + throw new Error("No framework configuration defined. Make sure to also provide the framework name."); + } + if (project.framework && !project.framework.name) { + // This should not happen as the configuration should have been validated against the schema + throw new Error(`Project ${project.metadata.name} does not define a framework name configuration`); + } + if (frameworkOptions.name) { + if (frameworkOptions.name.toLowerCase() === "openui5") { + return "OpenUI5"; + } else if (frameworkOptions.name.toLowerCase() === "sapui5") { + return "SAPUI5"; + } else { + throw new Error("Invalid framework name: " + frameworkOptions.name); + } + } else { + return project.framework.name; + } +} + +module.exports = async function({normalizerOptions, frameworkOptions}) { + const project = await getRootProjectConfiguration({normalizerOptions}); + + if (!isValidSpecVersion(project.specVersion)) { + throw new Error( + `ui5 use command requires specVersion "2.0" or higher. ` + + `Project ${project.metadata.name} uses specVersion "${project.specVersion}"` + ); + } + + const framework = { + name: getEffectiveFrameworkName({project, frameworkOptions}) + }; + + const frameworkVersion = frameworkOptions.version || (project.framework && project.framework.version); + if (frameworkVersion) { + framework.version = await resolveVersion({ + frameworkName: framework.name, + frameworkVersion + }, { + cwd: project.path + }); + } + + // Try to update YAML file but still return with name and resolved version in case it failed + let yamlUpdated = false; + try { + await require("./updateYaml")({ + project, + data: { + framework: framework + } + }); + yamlUpdated = true; + } catch (err) { + if (err.name !== "FrameworkUpdateYamlFailed") { + throw err; + } + } + return { + yamlUpdated, + usedFramework: framework.name, + usedVersion: framework.version || null + }; +}; diff --git a/lib/framework/utils.js b/lib/framework/utils.js new file mode 100644 index 00000000..90c8b542 --- /dev/null +++ b/lib/framework/utils.js @@ -0,0 +1,28 @@ +module.exports = { + getRootProjectConfiguration: async function({normalizerOptions}) { + const {normalizer, projectPreprocessor} = require("@ui5/project"); + + const tree = await normalizer.generateDependencyTree(normalizerOptions); + + if (normalizerOptions.configPath) { + tree.configPath = normalizerOptions.configPath; + } + + // Prevent dependencies from being processed + tree.dependencies = []; + + return projectPreprocessor.processTree(tree); + }, + getFrameworkResolver: function(frameworkName) { + if (frameworkName === "SAPUI5") { + return require("@ui5/project").ui5Framework.Sapui5Resolver; + } else if (frameworkName === "OpenUI5") { + return require("@ui5/project").ui5Framework.Openui5Resolver; + } else { + throw new Error("Invalid framework.name: " + frameworkName); + } + }, + isValidSpecVersion: function(specVersion) { + return specVersion && (specVersion !== "0.1" && specVersion !== "1.0" && specVersion !== "1.1"); + } +}; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 3d9b139b..8114ef7c 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2756,6 +2756,14 @@ "assert-plus": "^1.0.0" } }, + "data-with-position": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/data-with-position/-/data-with-position-0.4.0.tgz", + "integrity": "sha512-pMjD0tsOtUrYaCn0oQs1WcHwsPiqCIb4OnO0kcCIdcnfUbPUU9hEok5ug7pLZHSrzgA3lQCoFQqeXRTe5RFE5w==", + "requires": { + "yaml-ast-parser": "^0.0.43" + } + }, "date-time": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/date-time/-/date-time-2.1.0.tgz", @@ -3179,9 +3187,9 @@ } }, "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", @@ -9015,6 +9023,11 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true }, + "yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==" + }, "yargs": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", diff --git a/package.json b/package.json index f77f6275..f436087a 100644 --- a/package.json +++ b/package.json @@ -108,10 +108,12 @@ "@ui5/project": "^2.0.0", "@ui5/server": "^2.0.0", "chalk": "^3.0.0", + "data-with-position": "^0.4.0", "import-local": "^3.0.2", "js-yaml": "^3.13.1", "open": "^7.0.3", "semver": "^7.1.3", + "string.prototype.matchall": "^4.0.2", "supports-color": "^7.1.0", "treeify": "^1.0.1", "update-notifier": "^4.1.0", diff --git a/test/lib/cli/commands/add.js b/test/lib/cli/commands/add.js new file mode 100644 index 00000000..d77eaacb --- /dev/null +++ b/test/lib/cli/commands/add.js @@ -0,0 +1,186 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +const addCommand = require("../../../../lib/cli/commands/add"); + +async function assertAddHandler(t, {argv, expectedLibraries, expectedConsoleLog}) { + const frameworkAddStub = sinon.stub().resolves({ + yamlUpdated: true + }); + mock("../../../../lib/framework/add", frameworkAddStub); + + await addCommand.handler(argv); + + t.is(frameworkAddStub.callCount, 1, "Add function should be called once"); + t.deepEqual(frameworkAddStub.getCall(0).args, [ + { + libraries: expectedLibraries, + normalizerOptions: { + configPath: undefined, + translatorName: undefined + } + }], + "Add function should be called with expected args"); + + t.is(t.context.consoleLogStub.callCount, expectedConsoleLog.length, + "console.log should be called " + expectedConsoleLog.length + " times"); + expectedConsoleLog.forEach((expectedLog, i) => { + t.deepEqual(t.context.consoleLogStub.getCall(i).args, [expectedLog], + "console.log should be called with expected string on call index " + i); + }); +} + +async function assertFailingAddHandler(t, {argv, expectedMessage}) { + const frameworkAddStub = sinon.stub().resolves({ + yamlUpdated: true + }); + mock("../../../../lib/framework/add", frameworkAddStub); + + const exception = await t.throwsAsync(addCommand.handler(argv)); + + t.is(exception.message, expectedMessage, "Add handler should throw expected error"); + t.is(frameworkAddStub.callCount, 0, "Add function should not be called"); +} + +async function assertFailingYamlUpdateAddHandler(t, {argv, expectedMessage}) { + const frameworkAddStub = sinon.stub().resolves({ + yamlUpdated: false + }); + mock("../../../../lib/framework/add", frameworkAddStub); + + const exception = await t.throwsAsync(addCommand.handler(argv)); + + t.is(exception.message, expectedMessage, "Add handler should throw expected error"); + t.is(frameworkAddStub.callCount, 1, "Add function should be called once"); +} + +test.beforeEach((t) => { + t.context.consoleLogStub = sinon.stub(console, "log"); +}); + +test.afterEach.always(() => { + mock.stopAll(); + sinon.restore(); +}); + +test.serial("Accepts single library", async (t) => { + await assertAddHandler(t, { + argv: {"framework-libraries": ["sap.ui.lib1"]}, + expectedLibraries: [{name: "sap.ui.lib1"}], + expectedConsoleLog: [ + "Updated configuration written to ui5.yaml", + "Added framework library sap.ui.lib1 as dependency" + ] + }); +}); + +test.serial("Accepts multiple libraries", async (t) => { + await assertAddHandler(t, { + argv: {"framework-libraries": ["sap.ui.lib1", "sap.ui.lib2"]}, + expectedLibraries: [{name: "sap.ui.lib1"}, {name: "sap.ui.lib2"}], + expectedConsoleLog: [ + "Updated configuration written to ui5.yaml", + "Added framework libraries sap.ui.lib1 sap.ui.lib2 as dependencies" + ] + }); +}); + +test.serial("Accepts multiple libraries (--development)", async (t) => { + await assertAddHandler(t, { + argv: { + "framework-libraries": [ + "sap.ui.lib1", + "sap.ui.lib2" + ], + "development": true + }, + expectedLibraries: [ + { + name: "sap.ui.lib1", + development: true + }, + { + name: "sap.ui.lib2", + development: true + } + ], + expectedConsoleLog: [ + "Updated configuration written to ui5.yaml", + "Added framework libraries sap.ui.lib1 sap.ui.lib2 as development dependencies" + ] + }); +}); + +test.serial("Accepts multiple libraries (--optional)", async (t) => { + await assertAddHandler(t, { + argv: { + "framework-libraries": [ + "sap.ui.lib1", + "sap.ui.lib2" + ], + "optional": true + }, + expectedLibraries: [ + { + name: "sap.ui.lib1", + optional: true + }, + { + name: "sap.ui.lib2", + optional: true + } + ], + expectedConsoleLog: [ + "Updated configuration written to ui5.yaml", + "Added framework libraries sap.ui.lib1 sap.ui.lib2 as optional dependencies" + ] + }); +}); + +test.serial("Rejects when development and optional are true", async (t) => { + await assertFailingAddHandler(t, { + argv: { + "framework-libraries": ["sap.ui.lib1"], + "development": true, + "optional": true + }, + expectedMessage: "Options 'development' and 'optional' cannot be combined" + }); +}); + +test.serial("Rejects on empty framework-libraries", async (t) => { + await assertFailingAddHandler(t, { + argv: {"framework-libraries": ""}, + expectedMessage: "Missing mandatory parameter framework-libraries" + }); +}); + +test.serial("Rejects when YAML could not be updated (single library)", async (t) => { + await assertFailingYamlUpdateAddHandler(t, { + argv: {"framework-libraries": ["sap.ui.lib1"]}, + expectedMessage: "Internal error while adding framework library sap.ui.lib1 to ui5.yaml" + }); +}); + +test.serial("Rejects when YAML could not be updated (multiple libraries)", async (t) => { + await assertFailingYamlUpdateAddHandler(t, { + argv: {"framework-libraries": ["sap.ui.lib1", "sap.ui.lib2"]}, + expectedMessage: "Internal error while adding framework libraries sap.ui.lib1 sap.ui.lib2 to ui5.yaml" + }); +}); + +test.serial("Rejects when YAML could not be updated (single library; with config path)", async (t) => { + await assertFailingYamlUpdateAddHandler(t, { + argv: {"framework-libraries": ["sap.ui.lib1"], "config": "/path/to/ui5.yaml"}, + expectedMessage: "Internal error while adding framework library sap.ui.lib1 to config at /path/to/ui5.yaml" + }); +}); + +test.serial("Rejects when YAML could not be updated (multiple libraries; with config path)", async (t) => { + await assertFailingYamlUpdateAddHandler(t, { + argv: {"framework-libraries": ["sap.ui.lib1", "sap.ui.lib2"], "config": "/path/to/ui5.yaml"}, + expectedMessage: + "Internal error while adding framework libraries sap.ui.lib1 sap.ui.lib2 to config at /path/to/ui5.yaml" + }); +}); diff --git a/test/lib/cli/commands/use.js b/test/lib/cli/commands/use.js new file mode 100644 index 00000000..54826f99 --- /dev/null +++ b/test/lib/cli/commands/use.js @@ -0,0 +1,228 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +const useCommand = require("../../../../lib/cli/commands/use"); + +async function assertUseHandler(t, {argv, expectedFrameworkOptions}) { + const frameworkUseStub = sinon.stub().resolves({ + usedFramework: undefined, // not required for this test + usedVersion: undefined, // not required for this test + yamlUpdated: true + }); + mock("../../../../lib/framework/use", frameworkUseStub); + + await useCommand.handler(argv); + + t.is(frameworkUseStub.callCount, 1, "Use function should be called once"); + t.deepEqual(frameworkUseStub.getCall(0).args, [ + { + frameworkOptions: expectedFrameworkOptions, + normalizerOptions: { + configPath: undefined, + translatorName: undefined + } + }], + "Use function should be called with expected args"); +} + +async function assertFailingUseHandler(t, {argv, expectedMessage}) { + const frameworkUseStub = sinon.stub().resolves({ + usedFramework: undefined, // not required for this test + usedVersion: undefined // not required for this test + }); + mock("../../../../lib/framework/use", frameworkUseStub); + + const exception = await t.throwsAsync(useCommand.handler(argv)); + + t.is(exception.message, expectedMessage, "Use handler should throw expected error"); + t.is(frameworkUseStub.callCount, 0, "Use function should not be called"); +} + +async function assertFailingYamlUpdateUseHandler(t, {argv, expectedMessage}) { + const frameworkUseStub = sinon.stub().resolves({ + usedFramework: "SAPUI5", + usedVersion: "1.76.0", + yamlUpdated: false + }); + mock("../../../../lib/framework/use", frameworkUseStub); + + const exception = await t.throwsAsync(useCommand.handler(argv)); + + t.is(exception.message, expectedMessage, "Use handler should throw expected error"); + t.is(frameworkUseStub.callCount, 1, "Use function should be called once"); +} + +test.afterEach.always(() => { + mock.stopAll(); + sinon.restore(); +}); + +test.serial("Accepts framework name and version (SAPUI5@1.76.0)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "SAPUI5@1.76.0"}, + expectedFrameworkOptions: { + name: "SAPUI5", + version: "1.76.0" + } + }); +}); + +test.serial("Accepts framework name and version (OpenUI5@1.76.0)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "OpenUI5@1.76.0"}, + expectedFrameworkOptions: { + name: "OpenUI5", + version: "1.76.0" + } + }); +}); + +test.serial("Accepts framework name and version (SAPUI5@1.76)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "SAPUI5@1.76"}, + expectedFrameworkOptions: { + name: "SAPUI5", + version: "1.76" + } + }); +}); + +test.serial("Accepts framework name and version (OpenUI5@1.76)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "OpenUI5@1.76"}, + expectedFrameworkOptions: { + name: "OpenUI5", + version: "1.76" + } + }); +}); + +test.serial("Accepts framework name and version (SAPUI5@latest)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "SAPUI5@latest"}, + expectedFrameworkOptions: { + name: "SAPUI5", + version: "latest" + } + }); +}); + +test.serial("Accepts framework name and version (OpenUI5@latest)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "OpenUI5@latest"}, + expectedFrameworkOptions: { + name: "OpenUI5", + version: "latest" + } + }); +}); + +test.serial("Accepts framework name (SAPUI5)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "SAPUI5"}, + expectedFrameworkOptions: { + name: "SAPUI5", + version: null + } + }); +}); + +test.serial("Accepts framework name (sapui5)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "sapui5"}, + expectedFrameworkOptions: { + name: "sapui5", + version: null + } + }); +}); + +test.serial("Accepts framework name (OpenUI5)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "OpenUI5"}, + expectedFrameworkOptions: { + name: "OpenUI5", + version: null + } + }); +}); + +test.serial("Accepts framework version (1.76.0)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "1.76.0"}, + expectedFrameworkOptions: { + name: null, + version: "1.76.0" + } + }); +}); + +test.serial("Accepts framework version (1.76)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "1.76"}, + expectedFrameworkOptions: { + name: null, + version: "1.76" + } + }); +}); + +test.serial("Accepts framework version (latest)", async (t) => { + await assertUseHandler(t, { + argv: {"framework-info": "latest"}, + expectedFrameworkOptions: { + name: null, + version: "latest" + } + }); +}); + +test.serial("Rejects on empty framework-info", async (t) => { + await assertFailingUseHandler(t, { + argv: {"framework-info": ""}, + expectedMessage: "Invalid framework info: " + }); +}); + +test.serial("Rejects on invalid framework-info (@1.2.3)", async (t) => { + await assertFailingUseHandler(t, { + argv: {"framework-info": "@1.2.3"}, + expectedMessage: "Invalid framework info: @1.2.3" + }); +}); + +test.serial("Rejects on invalid framework-info (SAPUI5@)", async (t) => { + await assertFailingUseHandler(t, { + argv: {"framework-info": "SAPUI5@"}, + expectedMessage: "Invalid framework info: SAPUI5@" + }); +}); + +test.serial("Rejects on invalid framework-info (@SAPUI5@)", async (t) => { + await assertFailingUseHandler(t, { + argv: {"framework-info": "@SAPUI5@"}, + expectedMessage: "Invalid framework info: @SAPUI5@" + }); +}); + +test.serial("Rejects on invalid framework-info (SAPUI5@1.2.3@4.5.6)", async (t) => { + await assertFailingUseHandler(t, { + argv: {"framework-info": "SAPUI5@1.2.3@4.5.6"}, + expectedMessage: "Invalid framework info: SAPUI5@1.2.3@4.5.6" + }); +}); + +test.serial("Rejects when YAML could not be updated", async (t) => { + await assertFailingYamlUpdateUseHandler(t, { + argv: {"framework-info": "SAPUI5@1.76.0"}, + expectedMessage: "Internal error while updating ui5.yaml to SAPUI5 version 1.76.0" + }); +}); + +test.serial("Rejects when YAML could not be updated (with config path)", async (t) => { + await assertFailingYamlUpdateUseHandler(t, { + argv: {"framework-info": "SAPUI5@1.76.0", "config": "/path/to/ui5.yaml"}, + expectedMessage: "Internal error while updating config at /path/to/ui5.yaml to SAPUI5 version 1.76.0" + }); +}); diff --git a/test/lib/framework/add.js b/test/lib/framework/add.js new file mode 100644 index 00000000..7e892ebd --- /dev/null +++ b/test/lib/framework/add.js @@ -0,0 +1,564 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +const ui5Project = require("@ui5/project"); + +let addFramework; + +test.beforeEach((t) => { + t.context.generateDependencyTreeStub = sinon.stub(ui5Project.normalizer, "generateDependencyTree"); + t.context.processTreeStub = sinon.stub(ui5Project.projectPreprocessor, "processTree"); + t.context.Openui5GetLibraryMetadataStub = sinon.stub( + ui5Project.ui5Framework.Openui5Resolver.prototype, "getLibraryMetadata"); + t.context.Sapui5GetLibraryMetadataStub = sinon.stub( + ui5Project.ui5Framework.Sapui5Resolver.prototype, "getLibraryMetadata"); + + t.context.updateYamlStub = sinon.stub(); + mock("../../../lib/framework/updateYaml", t.context.updateYamlStub); + + addFramework = mock.reRequire("../../../lib/framework/add"); +}); + +test.afterEach.always(() => { + mock.stopAll(); + sinon.restore(); +}); + +test.serial("Add without existing libraries in config", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Openui5GetLibraryMetadataStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + framework: { + name: "OpenUI5", + version: "1.76.0" + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + Openui5GetLibraryMetadataStub.resolves(); + + const result = await addFramework({ + normalizerOptions, + libraries: [{name: "sap.ui.lib1"}] + }); + + t.deepEqual(result, {yamlUpdated: true}, "yamlUpdated should be true"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5GetLibraryMetadataStub.callCount, 1, "Openui5Resolver.getLibraryMetadata should be called once"); + t.deepEqual(Openui5GetLibraryMetadataStub.getCall(0).args, ["sap.ui.lib1"], + "Openui5Resolver.getLibraryMetadata should be called with expected args"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: {libraries: [{name: "sap.ui.lib1"}]} + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Add with existing libraries in config", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Openui5GetLibraryMetadataStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + framework: { + name: "OpenUI5", + version: "1.76.0", + libraries: [{ + name: "sap.ui.lib2" + }, { + name: "sap.ui.lib1" + }] + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + Openui5GetLibraryMetadataStub.resolves(); + + const result = await addFramework({ + normalizerOptions, + libraries: [{name: "sap.ui.lib1"}, {name: "sap.ui.lib3"}] + }); + + t.deepEqual(result, {yamlUpdated: true}, "yamlUpdated should be true"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5GetLibraryMetadataStub.callCount, 2, "Openui5Resolver.getLibraryMetadata should be called twice"); + t.deepEqual(Openui5GetLibraryMetadataStub.getCall(0).args, ["sap.ui.lib1"], + "Openui5Resolver.getLibraryMetadata should be called with expected args on first call"); + t.deepEqual(Openui5GetLibraryMetadataStub.getCall(1).args, ["sap.ui.lib3"], + "Openui5Resolver.getLibraryMetadata should be called with expected args on second call"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + libraries: [ + {name: "sap.ui.lib1"}, + {name: "sap.ui.lib2"}, + {name: "sap.ui.lib3"} + ] + } + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Add optional with existing libraries in config", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Openui5GetLibraryMetadataStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + framework: { + name: "OpenUI5", + version: "1.76.0", + libraries: [{ + name: "sap.ui.lib2", + development: true + }, { + name: "sap.ui.lib1", + development: true + }] + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + Openui5GetLibraryMetadataStub.resolves(); + + const result = await addFramework({ + normalizerOptions, + libraries: [{name: "sap.ui.lib1", optional: true}, {name: "sap.ui.lib3", optional: true}] + }); + + t.deepEqual(result, {yamlUpdated: true}, "yamlUpdated should be true"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5GetLibraryMetadataStub.callCount, 2, "Openui5Resolver.getLibraryMetadata should be called twice"); + t.deepEqual(Openui5GetLibraryMetadataStub.getCall(0).args, ["sap.ui.lib1"], + "Openui5Resolver.getLibraryMetadata should be called with expected args on first call"); + t.deepEqual(Openui5GetLibraryMetadataStub.getCall(1).args, ["sap.ui.lib3"], + "Openui5Resolver.getLibraryMetadata should be called with expected args on second call"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + libraries: [ + {name: "sap.ui.lib1", optional: true}, + {name: "sap.ui.lib2", development: true}, + {name: "sap.ui.lib3", optional: true} + ] + } + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Add with specVersion 1.0", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Openui5GetLibraryMetadataStub, Sapui5GetLibraryMetadataStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "1.0", + metadata: { + name: "my-project" + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const error = await t.throwsAsync(addFramework({ + normalizerOptions + })); + + t.is(error.message, + `ui5 add command requires specVersion "2.0" or higher. Project my-project uses specVersion "1.0"`); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5GetLibraryMetadataStub.callCount, 0, "Openui5Resolver.getLibraryMetadata should not be called"); + t.is(Sapui5GetLibraryMetadataStub.callCount, 0, "Sapui5Resolver.getLibraryMetadata should not be called"); + + t.is(updateYamlStub.callCount, 0, "updateYaml should not be called"); +}); + +test.serial("Add without framework configuration", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Openui5GetLibraryMetadataStub, Sapui5GetLibraryMetadataStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const error = await t.throwsAsync(addFramework({ + normalizerOptions + })); + + t.is(error.message, `Project my-project is missing a framework configuration. ` + + `Please use "ui5 use" to configure a framework and version.`); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5GetLibraryMetadataStub.callCount, 0, "Openui5Resolver.getLibraryMetadata should not be called"); + t.is(Sapui5GetLibraryMetadataStub.callCount, 0, "Sapui5Resolver.getLibraryMetadata should not be called"); + + t.is(updateYamlStub.callCount, 0, "updateYaml should not be called"); +}); + +test.serial("Add without framework version configuration", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Openui5GetLibraryMetadataStub, Sapui5GetLibraryMetadataStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + framework: { + name: "OpenUI5" + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const error = await t.throwsAsync(addFramework({ + normalizerOptions + })); + + t.is(error.message, `Project my-project does not define a framework version configuration. ` + + `Please use "ui5 use" to configure a version.`); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5GetLibraryMetadataStub.callCount, 0, "Openui5Resolver.getLibraryMetadata should not be called"); + t.is(Sapui5GetLibraryMetadataStub.callCount, 0, "Sapui5Resolver.getLibraryMetadata should not be called"); + + t.is(updateYamlStub.callCount, 0, "updateYaml should not be called"); +}); + +test.serial("Add with failing library metadata call", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Sapui5GetLibraryMetadataStub, updateYamlStub} = t.context; + + Sapui5GetLibraryMetadataStub.rejects(new Error("Failed to load library sap.ui.lib1")); + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + framework: { + name: "SAPUI5", + version: "1.76.0" + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const error = await t.throwsAsync(addFramework({ + normalizerOptions, + libraries: [{name: "sap.ui.lib1"}] + })); + + t.is(error.message, `Failed to find SAPUI5 framework library sap.ui.lib1: Failed to load library sap.ui.lib1`); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Sapui5GetLibraryMetadataStub.callCount, 1, "Sapui5Resolver.getLibraryMetadata should be called once"); + t.deepEqual(Sapui5GetLibraryMetadataStub.getCall(0).args, ["sap.ui.lib1"], + "Sapui5Resolver.getLibraryMetadata should be called with expected args on first call"); + + t.is(updateYamlStub.callCount, 0, "updateYaml should not be called"); +}); + +test.serial("Add with failing YAML update", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Sapui5GetLibraryMetadataStub, updateYamlStub} = t.context; + + const yamlUpdateError = new Error("Failed to update YAML file"); + yamlUpdateError.name = "FrameworkUpdateYamlFailed"; + updateYamlStub.rejects(yamlUpdateError); + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + framework: { + name: "SAPUI5", + version: "1.76.0" + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const result = await addFramework({ + normalizerOptions, + libraries: [{name: "sap.ui.lib1"}] + }); + + t.deepEqual(result, {yamlUpdated: false}, "yamlUpdated should be false"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Sapui5GetLibraryMetadataStub.callCount, 1, "Sapui5Resolver.getLibraryMetadata should be called once"); + t.deepEqual(Sapui5GetLibraryMetadataStub.getCall(0).args, ["sap.ui.lib1"], + "Sapui5Resolver.getLibraryMetadata should be called with expected args on first call"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + libraries: [{name: "sap.ui.lib1"}] + } + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Add with failing YAML update (unexpected error)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Sapui5GetLibraryMetadataStub, updateYamlStub} = t.context; + + updateYamlStub.rejects(new Error("Some unexpected error")); + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + framework: { + name: "SAPUI5", + version: "1.76.0" + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const error = await t.throwsAsync(addFramework({ + normalizerOptions, + libraries: [{name: "sap.ui.lib1"}] + })); + + t.is(error.message, `Some unexpected error`); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Sapui5GetLibraryMetadataStub.callCount, 1, "Sapui5Resolver.getLibraryMetadata should be called once"); + t.deepEqual(Sapui5GetLibraryMetadataStub.getCall(0).args, ["sap.ui.lib1"], + "Sapui5Resolver.getLibraryMetadata should be called with expected args on first call"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + libraries: [{name: "sap.ui.lib1"}] + } + } + }], "updateYaml should be called with expected args"); +}); + + +test.serial("Add should not modify input parameters", async (t) => { + const {generateDependencyTreeStub, processTreeStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + framework: { + name: "SAPUI5", + version: "1.76.0", + libraries: [{"name": "sap.ui.lib1"}] + } + }; + + const libraries = [{name: "sap.ui.lib2"}]; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + await addFramework({ + normalizerOptions, + libraries + }); + + t.deepEqual(libraries, [{name: "sap.ui.lib2"}], "libraries array should not be changed"); +}); diff --git a/test/lib/framework/updateYaml.js b/test/lib/framework/updateYaml.js new file mode 100644 index 00000000..d4422561 --- /dev/null +++ b/test/lib/framework/updateYaml.js @@ -0,0 +1,511 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +const fs = require("fs"); +const path = require("path"); + +let updateYaml; + +test.beforeEach((t) => { + t.context.fsReadFileStub = sinon.stub(fs, "readFile"); + t.context.fsWriteFileStub = sinon.stub(fs, "writeFile").yieldsAsync(null); + + updateYaml = mock.reRequire("../../../lib/framework/updateYaml"); +}); + +test.afterEach.always(() => { + mock.stopAll(); + sinon.restore(); +}); + +test.serial("Should update single document", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +--- +metadata: + name: my-project +framework: + name: SAPUI5 + version: 1.0.0 +`); + + await updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + name: "OpenUI5", + version: "1.76.0" + } + } + }); + + t.is(t.context.fsWriteFileStub.callCount, 1, "fs.writeFile should be called once"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[0], path.join("my-project", "ui5.yaml"), + "writeFile should be called with expected path"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[1], ` +--- +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" +`, "writeFile should be called with expected content"); +}); + +test.serial("Should update first document", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +specVersion: "2.0" +metadata: + name: my-project +framework: + name: SAPUI5 + version: 1.0.0 +--- +specVersion: "1.0" +kind: extension +metadata: + name: my-extension +type: project-shim +shims: + configurations: {} +`); + + await updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + name: "OpenUI5", + version: "1.76.0" + } + } + }); + + t.is(t.context.fsWriteFileStub.callCount, 1, "fs.writeFile should be called once"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[0], path.join("my-project", "ui5.yaml"), + "writeFile should be called with expected path"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[1], ` +specVersion: "2.0" +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" +--- +specVersion: "1.0" +kind: extension +metadata: + name: my-extension +type: project-shim +shims: + configurations: {} +`, "writeFile should be called with expected content"); +}); + + +test.serial("Should update second document", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +specVersion: "1.0" +kind: extension +metadata: + name: my-extension +type: project-shim +shims: + configurations: {} +framework: + name: SAPUI5 + version: 1.0.0 +--- +specVersion: "2.0" +metadata: + name: my-project +framework: + name: SAPUI5 + version: 1.0.0 +`); + + await updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + name: "OpenUI5", + version: "1.76.0" + } + } + }); + + t.is(t.context.fsWriteFileStub.callCount, 1, "fs.writeFile should be called once"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[0], path.join("my-project", "ui5.yaml"), + "writeFile should be called with expected path"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[1], ` +specVersion: "1.0" +kind: extension +metadata: + name: my-extension +type: project-shim +shims: + configurations: {} +framework: + name: SAPUI5 + version: 1.0.0 +--- +specVersion: "2.0" +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" +`, "writeFile should be called with expected content"); +}); + +test.serial("Should add new object with one property to document", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +metadata: + name: my-project`); + + await updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + name: "OpenUI5" + } + } + }); + + t.is(t.context.fsWriteFileStub.callCount, 1, "fs.writeFile should be called once"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[0], path.join("my-project", "ui5.yaml"), + "writeFile should be called with expected path"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[1], ` +metadata: + name: my-project +framework: + name: OpenUI5 +`, "writeFile should be called with expected content"); +}); + +test.serial("Should add new object with two properties to document", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +metadata: + name: my-project`); + + await updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + name: "OpenUI5", + version: "1.76.0" + } + } + }); + + t.is(t.context.fsWriteFileStub.callCount, 1, "fs.writeFile should be called once"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[0], path.join("my-project", "ui5.yaml"), + "writeFile should be called with expected path"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[1], ` +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" +`, "writeFile should be called with expected content"); +}); + +test.serial("Should add version property to document and keep name", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +metadata: + name: my-project +framework: + name: "OpenUI5" +`); + + await updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + name: "OpenUI5", + version: "1.76.0" + } + } + }); + + t.is(t.context.fsWriteFileStub.callCount, 1, "fs.writeFile should be called once"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[0], path.join("my-project", "ui5.yaml"), + "writeFile should be called with expected path"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[1], ` +metadata: + name: my-project +framework: + name: "OpenUI5" + version: "1.76.0" +`, "writeFile should be called with expected content"); +}); + +test.serial("Should add name property to document and keep version", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +metadata: + name: my-project +framework: + version: 1.76.0 +`); + + await updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + name: "OpenUI5", + version: "1.76.0" + } + } + }); + + t.is(t.context.fsWriteFileStub.callCount, 1, "fs.writeFile should be called once"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[0], path.join("my-project", "ui5.yaml"), + "writeFile should be called with expected path"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[1], ` +metadata: + name: my-project +framework: + version: 1.76.0 + name: OpenUI5 +`, "writeFile should be called with expected content"); +}); + +test.serial("Should add new array to document", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" +`); + + await updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + libraries: [ + {name: "sap.ui.core"}, + {name: "sap.m"} + ] + } + } + }); + + t.is(t.context.fsWriteFileStub.callCount, 1, "fs.writeFile should be called once"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[0], path.join("my-project", "ui5.yaml"), + "writeFile should be called with expected path"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[1], ` +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" + libraries: + - name: sap.ui.core + - name: sap.m +`, "writeFile should be called with expected content"); +}); + +test.serial("Should add new array element to document", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" + libraries: + - name: sap.ui.core +`); + + await updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + libraries: [ + {name: "sap.ui.core"}, + {name: "sap.m"} + ] + } + } + }); + + t.is(t.context.fsWriteFileStub.callCount, 1, "fs.writeFile should be called once"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[0], path.join("my-project", "ui5.yaml"), + "writeFile should be called with expected path"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[1], ` +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" + libraries: + - name: sap.ui.core + - name: sap.m +`, "writeFile should be called with expected content"); +}); + +test.serial("Should add new array elements to document", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" + libraries: + - name: sap.ui.core +`); + + await updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + libraries: [ + {name: "sap.ui.core"}, + {name: "sap.m"}, + {name: "sap.ui.layout"} + ] + } + } + }); + + t.is(t.context.fsWriteFileStub.callCount, 1, "fs.writeFile should be called once"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[0], path.join("my-project", "ui5.yaml"), + "writeFile should be called with expected path"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[1], ` +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" + libraries: + - name: sap.ui.core + - name: sap.m + - name: sap.ui.layout +`, "writeFile should be called with expected content"); +}); + +test.serial("Should add new array elements with multiple properties to document", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" + libraries: + - name: sap.ui.core + development: true +`); + + await updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + libraries: [ + {name: "sap.ui.core", optional: true}, + {name: "sap.m", optional: true}, + {name: "sap.ui.layout", optional: true} + ] + } + } + }); + + t.is(t.context.fsWriteFileStub.callCount, 1, "fs.writeFile should be called once"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[0], path.join("my-project", "ui5.yaml"), + "writeFile should be called with expected path"); + t.deepEqual(t.context.fsWriteFileStub.getCall(0).args[1], ` +metadata: + name: my-project +framework: + name: OpenUI5 + version: "1.76.0" + libraries: + - name: sap.ui.core + optional: true + - name: sap.m + optional: true + - name: sap.ui.layout + optional: true +`, "writeFile should be called with expected content"); +}); + +test.serial("Should validate YAML before writing file", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +metadata: + name: my-project +framework: { name: "SAPUI5" } +`); // Using JSON object notation is currently not supported + + const error = await t.throwsAsync(updateYaml({ + project: { + path: "my-project", + metadata: {"name": "my-project"} + }, + data: { + framework: { + name: "SAPUI5", + version: "1.76.0" + } + } + })); + + t.is(error.message, + "Failed to update YAML file: bad indentation of a mapping entry at line 5, column 14:\n" + + " version: \"1.76.0\"\n" + + " ^" + ); + t.is(t.context.fsWriteFileStub.callCount, 0, "fs.writeFile should not be called"); +}); + +test.serial("Should throw error when project document can't be found", async (t) => { + t.context.fsReadFileStub.yieldsAsync(null, ` +metadata: + name: my-project-1 +--- +metadata: + name: my-project-2 +`); + + const error = await t.throwsAsync(updateYaml({ + project: { + path: "my-project", + configPath: "ui5.yaml", + metadata: {"name": "my-project-3"} + }, + data: {} + })); + + t.is(error.message, "Could not find project with name my-project-3 in YAML: ui5.yaml"); + t.is(t.context.fsWriteFileStub.callCount, 0, "fs.writeFile should not be called"); +}); diff --git a/test/lib/framework/use.js b/test/lib/framework/use.js new file mode 100644 index 00000000..5cfdcd7f --- /dev/null +++ b/test/lib/framework/use.js @@ -0,0 +1,837 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +const ui5Project = require("@ui5/project"); + +let useFramework; + +test.beforeEach((t) => { + t.context.generateDependencyTreeStub = sinon.stub(ui5Project.normalizer, "generateDependencyTree"); + t.context.processTreeStub = sinon.stub(ui5Project.projectPreprocessor, "processTree"); + t.context.Openui5ResolveVersionStub = sinon.stub(ui5Project.ui5Framework.Openui5Resolver, "resolveVersion"); + t.context.Sapui5ResolveVersionStub = sinon.stub(ui5Project.ui5Framework.Sapui5Resolver, "resolveVersion"); + + t.context.updateYamlStub = sinon.stub(); + mock("../../../lib/framework/updateYaml", t.context.updateYamlStub); + + useFramework = mock.reRequire("../../../lib/framework/use"); +}); + +test.afterEach.always(() => { + mock.stopAll(); + sinon.restore(); +}); + +test.serial("Use with name and version (OpenUI5)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, Openui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project" + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + Openui5ResolveVersionStub.resolves("1.76.0"); + + const result = await useFramework({ + normalizerOptions, + frameworkOptions: { + name: "openui5", + version: "latest" + } + }); + + t.deepEqual(result, { + usedFramework: "OpenUI5", + usedVersion: "1.76.0", + yamlUpdated: true + }, "useFramework should return expected result object"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5ResolveVersionStub.callCount, 1, "Openui5Resolver.resolveVersion should be called once"); + t.deepEqual(Openui5ResolveVersionStub.getCall(0).args, ["latest", {cwd: "my-project"}], + "Openui5Resolver.resolveVersion should be called with expected args"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + name: "OpenUI5", + version: "1.76.0" + } + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Use with name and version (SAPUI5)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, Sapui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project" + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + Sapui5ResolveVersionStub.resolves("1.76.0"); + + const result = await useFramework({ + normalizerOptions, + frameworkOptions: { + name: "sapui5", + version: "latest" + } + }); + + t.deepEqual(result, { + usedFramework: "SAPUI5", + usedVersion: "1.76.0", + yamlUpdated: true + }, "useFramework should return expected result object"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Sapui5ResolveVersionStub.callCount, 1, "Sapui5Resolver.resolveVersion should be called once"); + t.deepEqual(Sapui5ResolveVersionStub.getCall(0).args, ["latest", {cwd: "my-project"}], + "Sapui5Resolver.resolveVersion should be called with expected args"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + name: "SAPUI5", + version: "1.76.0" + } + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Use with version only (OpenUI5)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, Openui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project", + framework: { + name: "OpenUI5" + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + Openui5ResolveVersionStub.resolves("1.76.0"); + + const result = await useFramework({ + normalizerOptions, + frameworkOptions: { + name: null, + version: "latest" + } + }); + + t.deepEqual(result, { + usedFramework: "OpenUI5", + usedVersion: "1.76.0", + yamlUpdated: true + }, "useFramework should return expected result object"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5ResolveVersionStub.callCount, 1, "Openui5Resolver.resolveVersion should be called once"); + t.deepEqual(Openui5ResolveVersionStub.getCall(0).args, ["latest", {cwd: "my-project"}], + "Openui5Resolver.resolveVersion should be called with expected args"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + name: "OpenUI5", + version: "1.76.0" + } + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Use with version only (SAPUI5)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, Sapui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project", + framework: { + name: "SAPUI5" + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + Sapui5ResolveVersionStub.resolves("1.76.0"); + + const result = await useFramework({ + normalizerOptions, + frameworkOptions: { + name: null, + version: "latest" + } + }); + + t.deepEqual(result, { + usedFramework: "SAPUI5", + usedVersion: "1.76.0", + yamlUpdated: true + }, "useFramework should return expected result object"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Sapui5ResolveVersionStub.callCount, 1, "Sapui5Resolver.resolveVersion should be called once"); + t.deepEqual(Sapui5ResolveVersionStub.getCall(0).args, ["latest", {cwd: "my-project"}], + "Sapui5Resolver.resolveVersion should be called with expected args"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + name: "SAPUI5", + version: "1.76.0" + } + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Use with name only (no existing framework configuration)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, Sapui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project" + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const result = await useFramework({ + normalizerOptions, + frameworkOptions: { + name: "SAPUI5", + version: null + } + }); + + t.deepEqual(result, { + usedFramework: "SAPUI5", + usedVersion: null, + yamlUpdated: true + }, "useFramework should return expected result object"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Sapui5ResolveVersionStub.callCount, 0, "Sapui5Resolver.resolveVersion should not be called"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + name: "SAPUI5" + } + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Use with name only (existing framework configuration)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, Sapui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project", + framework: { + name: "OpenUI5", + version: "1.76.0" + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + Sapui5ResolveVersionStub.resolves("1.76.0"); + + const result = await useFramework({ + normalizerOptions, + frameworkOptions: { + name: "SAPUI5", + version: null + } + }); + + t.deepEqual(result, { + usedFramework: "SAPUI5", + usedVersion: "1.76.0", + yamlUpdated: true + }, "useFramework should return expected result object"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Sapui5ResolveVersionStub.callCount, 1, "Sapui5Resolver.resolveVersion should be called once"); + t.deepEqual(Sapui5ResolveVersionStub.getCall(0).args, ["1.76.0", {cwd: "my-project"}], + "Sapui5Resolver.resolveVersion should be called with expected args"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + name: "SAPUI5", + version: "1.76.0" + } + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Use with normalizerOptions.configPath", async (t) => { + const {generateDependencyTreeStub, processTreeStub, Sapui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + configPath: "/path/to/ui5.yaml" + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project" + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + Sapui5ResolveVersionStub.resolves("1.76.0"); + + const result = await useFramework({ + normalizerOptions, + frameworkOptions: { + name: "SAPUI5", + version: "latest" + } + }); + + t.deepEqual(result, { + usedFramework: "SAPUI5", + usedVersion: "1.76.0", + yamlUpdated: true + }, "useFramework should return expected result object"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{configPath: "/path/to/ui5.yaml"}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + configPath: "/path/to/ui5.yaml", + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Sapui5ResolveVersionStub.callCount, 1, "Sapui5Resolver.resolveVersion should be called once"); + t.deepEqual(Sapui5ResolveVersionStub.getCall(0).args, ["latest", {cwd: "my-project"}], + "Sapui5Resolver.resolveVersion should be called with expected args"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + name: "SAPUI5", + version: "1.76.0" + } + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Use with version only (no framework name)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Openui5ResolveVersionStub, Sapui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project" + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const error = await t.throwsAsync(useFramework({ + normalizerOptions, + frameworkOptions: { + name: null, + version: "latest" + } + })); + + t.is(error.message, "No framework configuration defined. Make sure to also provide the framework name."); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5ResolveVersionStub.callCount, 0, "Openui5Resolver.resolveVersion should not be called"); + t.is(Sapui5ResolveVersionStub.callCount, 0, "Sapui5Resolver.resolveVersion should not be called"); + + t.is(updateYamlStub.callCount, 0, "updateYaml should not be called"); +}); + +test.serial("Use with invalid name", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Openui5ResolveVersionStub, Sapui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project" + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const error = await t.throwsAsync(useFramework({ + normalizerOptions, + frameworkOptions: { + name: "Foo", + version: "latest" + } + })); + + t.is(error.message, "Invalid framework name: Foo"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5ResolveVersionStub.callCount, 0, "Openui5Resolver.resolveVersion should not be called"); + t.is(Sapui5ResolveVersionStub.callCount, 0, "Sapui5Resolver.resolveVersion should not be called"); + + t.is(updateYamlStub.callCount, 0, "updateYaml should not be called"); +}); + +test.serial("Use with specVersion 1.0", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Openui5ResolveVersionStub, Sapui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "1.0", + metadata: { + name: "my-project" + }, + path: "my-project" + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const error = await t.throwsAsync(useFramework({ + normalizerOptions, + frameworkOptions: { + name: "Foo", + version: "latest" + } + })); + + t.is(error.message, + `ui5 use command requires specVersion "2.0" or higher. Project my-project uses specVersion "1.0"`); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5ResolveVersionStub.callCount, 0, "Openui5Resolver.resolveVersion should not be called"); + t.is(Sapui5ResolveVersionStub.callCount, 0, "Sapui5Resolver.resolveVersion should not be called"); + + t.is(updateYamlStub.callCount, 0, "updateYaml should not be called"); +}); + +test.serial("Use without framework.name (should actually be validated via ui5-project)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Openui5ResolveVersionStub, Sapui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project", + framework: {} + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const error = await t.throwsAsync(useFramework({ + normalizerOptions, + frameworkOptions: { + name: "SAPUI5", + version: "latest" + } + })); + + t.is(error.message, + `Project my-project does not define a framework name configuration`); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5ResolveVersionStub.callCount, 0, "Openui5Resolver.resolveVersion should not be called"); + t.is(Sapui5ResolveVersionStub.callCount, 0, "Sapui5Resolver.resolveVersion should not be called"); + + t.is(updateYamlStub.callCount, 0, "updateYaml should not be called"); +}); + +test.serial("Use with invalid framework name in config (should actually be validated via ui5-project)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, + Openui5ResolveVersionStub, Sapui5ResolveVersionStub, updateYamlStub} = t.context; + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project", + framework: { + name: "Foo" + } + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + + const error = await t.throwsAsync(useFramework({ + normalizerOptions, + frameworkOptions: { + name: null, + version: "latest" + } + })); + + t.is(error.message, "Invalid framework.name: Foo"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5ResolveVersionStub.callCount, 0, "Openui5Resolver.resolveVersion should not be called"); + t.is(Sapui5ResolveVersionStub.callCount, 0, "Sapui5Resolver.resolveVersion should not be called"); + + t.is(updateYamlStub.callCount, 0, "updateYaml should not be called"); +}); + +test.serial("Use with name and version (YAML update fails)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, Openui5ResolveVersionStub, updateYamlStub} = t.context; + + const yamlUpdateError = new Error("Failed to update YAML file"); + yamlUpdateError.name = "FrameworkUpdateYamlFailed"; + updateYamlStub.rejects(yamlUpdateError); + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project" + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + Openui5ResolveVersionStub.resolves("1.76.0"); + + const result = await useFramework({ + normalizerOptions, + frameworkOptions: { + name: "openui5", + version: "latest" + } + }); + + t.deepEqual(result, { + usedFramework: "OpenUI5", + usedVersion: "1.76.0", + yamlUpdated: false + }, "useFramework should return expected result object"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5ResolveVersionStub.callCount, 1, "Openui5Resolver.resolveVersion should be called once"); + t.deepEqual(Openui5ResolveVersionStub.getCall(0).args, ["latest", {cwd: "my-project"}], + "Openui5Resolver.resolveVersion should be called with expected args"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + name: "OpenUI5", + version: "1.76.0" + } + } + }], "updateYaml should be called with expected args"); +}); + +test.serial("Use with name and version (YAML update fails with unexpected error)", async (t) => { + const {generateDependencyTreeStub, processTreeStub, Openui5ResolveVersionStub, updateYamlStub} = t.context; + + updateYamlStub.rejects(new Error("Some unexpected error")); + + const normalizerOptions = { + "fakeNormalizerOption": true + }; + + const tree = { + dependencies: [{id: "fake-dependency"}] + }; + const project = { + specVersion: "2.0", + metadata: { + name: "my-project" + }, + path: "my-project" + }; + + generateDependencyTreeStub.resolves(tree); + processTreeStub.resolves(project); + Openui5ResolveVersionStub.resolves("1.76.0"); + + const error = await t.throwsAsync(useFramework({ + normalizerOptions, + frameworkOptions: { + name: "openui5", + version: "latest" + } + })); + + t.is(error.message, "Some unexpected error"); + + t.is(generateDependencyTreeStub.callCount, 1, "normalizer.generateDependencyTree should be called once"); + t.deepEqual(generateDependencyTreeStub.getCall(0).args, [{"fakeNormalizerOption": true}], + "normalizer.generateDependencyTree should be called with expected args"); + + t.is(processTreeStub.callCount, 1, "projectPreprocessor.processTree should be called once"); + t.deepEqual(processTreeStub.getCall(0).args, [{ + dependencies: [] + }], + "projectPreprocessor.processTree should be called with expected args"); + + t.is(Openui5ResolveVersionStub.callCount, 1, "Openui5Resolver.resolveVersion should be called once"); + t.deepEqual(Openui5ResolveVersionStub.getCall(0).args, ["latest", {cwd: "my-project"}], + "Openui5Resolver.resolveVersion should be called with expected args"); + + t.is(updateYamlStub.callCount, 1, "updateYaml should be called once"); + t.deepEqual(updateYamlStub.getCall(0).args, [{ + project, + data: { + framework: { + name: "OpenUI5", + version: "1.76.0" + } + } + }], "updateYaml should be called with expected args"); +});