From 28597af95a9299ffc44ef01d70c5b7bab69c053d Mon Sep 17 00:00:00 2001 From: Huajie Zhang Date: Fri, 25 Aug 2023 15:27:23 +0800 Subject: [PATCH] fix(cli): improve validation help message (#9725) * fix: rename list capabilities to templates * fix: question * fix: validation * refactor: move validation code from api into fx-core * fix: launch.json * fix: error * test: ut * test: ut --- packages/api/.nycrc | 2 +- packages/api/review/teamsfx-api.api.md | 26 +- packages/api/src/qm/question.ts | 64 +-- packages/api/src/qm/validation.ts | 211 --------- packages/api/tests/login.test.ts | 51 +-- packages/api/tests/qm.validation.test.ts | 411 ------------------ packages/api/tests/ui.test.ts | 75 ---- packages/cli/.vscode/launch.json | 2 +- packages/cli/src/constants.ts | 56 ++- packages/cli/src/questionUtils.ts | 13 +- packages/cli/src/utils.ts | 1 - .../cli/tests/unit/questionUtils.tests.ts | 39 +- packages/cli/tests/unit/utils.tests.ts | 28 +- .../component/middleware/actionExecutionMW.ts | 4 +- packages/fx-core/src/core/FxCore.ts | 6 +- packages/fx-core/src/index.ts | 1 + packages/fx-core/src/question/generator.ts | 2 +- packages/fx-core/src/question/other.ts | 1 + packages/fx-core/src/ui/validationUtils.ts | 216 ++++++++- packages/fx-core/src/ui/visitor.ts | 12 +- .../tests/question/question.spfx.test.ts | 2 +- packages/fx-core/tests/ui/qm.visitor.test.ts | 210 +++------ .../fx-core/tests/ui/validationUtils.test.ts | 393 ++++++++++++++++- packages/server/src/serverConnection.ts | 8 +- .../server/tests/serverConnection.test.ts | 9 +- .../vscode-extension/test/mocks/mockCore.ts | 2 - 26 files changed, 771 insertions(+), 1074 deletions(-) delete mode 100644 packages/api/tests/qm.validation.test.ts delete mode 100644 packages/api/tests/ui.test.ts diff --git a/packages/api/.nycrc b/packages/api/.nycrc index 3471e1a4f4..1a1c7b320f 100644 --- a/packages/api/.nycrc +++ b/packages/api/.nycrc @@ -4,5 +4,5 @@ "include": ["src/**/*.ts", "src/**/*.js"], "reporter": ["text", "html", "cobertura", "lcov"], "check-coverage": true, - "lines": 90 + "lines": 75 } diff --git a/packages/api/review/teamsfx-api.api.md b/packages/api/review/teamsfx-api.api.md index 8724921ab1..930c33575c 100644 --- a/packages/api/review/teamsfx-api.api.md +++ b/packages/api/review/teamsfx-api.api.md @@ -340,9 +340,6 @@ export interface FxError extends Error { userData?: any; } -// @public -export function getValidationFunction(validation: ValidationSchema, inputs: Inputs): (input: T) => string | undefined | Promise; - // @public export interface Group { // (undocumented) @@ -404,7 +401,7 @@ export interface IProgressHandler { start: (detail?: string) => Promise; } -// @public (undocumented) +// @public export interface IQTreeNode { // (undocumented) children?: IQTreeNode[]; @@ -615,24 +612,6 @@ export enum Platform { // @public (undocumented) export const ProductName = "teamsfx"; -// @public -export class QTreeNode implements IQTreeNode { - constructor(data: Question | Group); - // (undocumented) - addChild(node: QTreeNode): QTreeNode; - // (undocumented) - children?: QTreeNode[]; - cliOptionDisabled?: "self" | "children" | "all"; - // (undocumented) - condition?: StringValidation | StringArrayValidation | ConditionFunc; - // (undocumented) - data: Question | Group; - inputsDisabled?: "self" | "children" | "all"; - trim(): QTreeNode | undefined; - // (undocumented) - validate(): boolean; -} - // @public (undocumented) export type Question = SingleSelectQuestion | MultiSelectQuestion | TextInputQuestion | SingleFileQuestion | MultiFileQuestion | FolderQuestion | SingleFileQuestion | SingleFileOrInputQuestion; @@ -1103,9 +1082,6 @@ export interface UserInteraction { }>, modal: boolean, ...items: string[]): Promise>; } -// @public -export function validate(validSchema: ValidationSchema | ConditionFunc, value: T, inputs?: Inputs): Promise; - // @public (undocumented) export type ValidateFunc = (input: T, inputs?: Inputs) => string | undefined | Promise; diff --git a/packages/api/src/qm/question.ts b/packages/api/src/qm/question.ts index caa445669a..287e75117c 100644 --- a/packages/api/src/qm/question.ts +++ b/packages/api/src/qm/question.ts @@ -451,73 +451,11 @@ export type Question = | SingleFileOrInputQuestion; /** - * QTreeNode is the tree node data structure, which have three main properties: + * IQTreeNode is the tree node data structure, which have three main properties: * - data: data is either a group or question. Questions can be organized into a group, which has the same trigger condition. * - condition: trigger condition for this node to be activated; * - children: child questions that will be activated according their trigger condition. */ -export class QTreeNode implements IQTreeNode { - data: Question | Group; - condition?: StringValidation | StringArrayValidation | ConditionFunc; - children?: QTreeNode[]; - /** - * @description the question node will be ignored as CLI option in non-interactive mode - * "self" - only ignore the question itself - * "children" - ignore all nodes in sub-tree - * "all" - ignore self and all nodes in sub-tree - */ - cliOptionDisabled?: "self" | "children" | "all"; - /** - * @description the question node will be ignored as an Inputs property - * "self" - only ignore the question itself - * "children" - ignore all nodes in sub-tree - * "all" - ignore self and all nodes in sub-tree - */ - inputsDisabled?: "self" | "children" | "all"; - addChild(node: QTreeNode): QTreeNode { - if (!this.children) { - this.children = []; - } - this.children.push(node); - return this; - } - validate(): boolean { - //1. validate the cycle dependency - //2. validate the name uniqueness - //3. validate the params of RPC - // if (this.data.type === NodeType.group && (!this.children || this.children.length === 0)) return false; - return true; - } - - /** - * trim the tree - */ - trim(): QTreeNode | undefined { - if (this.children) { - const newChildren: QTreeNode[] = []; - for (const node of this.children) { - const trimmed = node.trim(); - if (trimmed) newChildren.push(trimmed); - } - this.children = newChildren; - } - if (this.data.type === "group") { - if (!this.children || this.children.length === 0) return undefined; - if (this.children.length === 1) { - const child = this.children[0]; - if (!(this.condition && child.condition)) { - child.condition ||= this.condition; - return child; - } - } - } - return this; - } - constructor(data: Question | Group) { - this.data = data; - } -} - export interface IQTreeNode { data: Question | Group; condition?: StringValidation | StringArrayValidation | ConditionFunc; diff --git a/packages/api/src/qm/validation.ts b/packages/api/src/qm/validation.ts index 18d7f3cb43..9a4a79e52a 100644 --- a/packages/api/src/qm/validation.ts +++ b/packages/api/src/qm/validation.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import * as jsonschema from "jsonschema"; import { Inputs, OptionItem } from "../types"; export type ValidateFunc = ( @@ -138,213 +137,3 @@ export type ConditionFunc = (inputs: Inputs) => boolean | Promise; * Definition of validation schema, which is a union of `StringValidation`, `StringArrayValidation` and `FuncValidation` */ export type ValidationSchema = StringValidation | StringArrayValidation | FuncValidation; - -/** - * A function to return a validation function according the validation schema - * @param validation validation schema - * @param inputs object to carry all user inputs - * @returns a validation function - */ -export function getValidationFunction( - validation: ValidationSchema, - inputs: Inputs -): (input: T) => string | undefined | Promise { - return function (input: T): string | undefined | Promise { - return validate(validation, input, inputs); - }; -} - -/** - * Implementation of validation function - * @param validSchema validation schema - * @param value value to validate - * @param inputs user inputs object, which works as the context of the validation - * @returns A human-readable string which is presented as diagnostic message. - * Return `undefined` when 'value' is valid. - */ -export async function validate( - validSchema: ValidationSchema | ConditionFunc, - value: T, - inputs?: Inputs -): Promise { - { - //FuncValidation - const funcValidation: FuncValidation = validSchema as FuncValidation; - if (funcValidation.validFunc) { - const res = await funcValidation.validFunc(value, inputs); - return res as string; - } else if (typeof funcValidation === "function") { - const res = await (funcValidation as ConditionFunc)(inputs!); - if (res) return undefined; - return "condition function is not met."; - } - } - - if (!value) { - if ((validSchema as StaticValidation).required === true) return `input value is required.`; - } - - const noneEmptyKeyNum = Object.keys(validSchema).filter( - (key) => (validSchema as any)[key] !== undefined - ).length; - - if (noneEmptyKeyNum === 0) { - return undefined; - } - - if ( - value === undefined && - ((validSchema as any).required || - (validSchema as any).equals || - (validSchema as any).maxLength || - (validSchema as any).minLength || - (validSchema as any).pattern || - (validSchema as any).enum || - (validSchema as any).startsWith || - (validSchema as any).endsWith || - (validSchema as any).includes || - (validSchema as any).maxItems || - (validSchema as any).minItems || - (validSchema as any).uniqueItems || - (validSchema as any).contains || - (validSchema as any).containsAll || - (validSchema as any).containsAny) - ) { - return `'undefined' does not meet condition:'${JSON.stringify(validSchema)}'`; - } - - { - // StringValidation - const stringValidation: StringValidation = validSchema as StringValidation; - const strToValidate = value as string; - if (strToValidate === undefined || typeof strToValidate === "string") { - const schema: any = {}; - if (stringValidation.equals && typeof stringValidation.equals === "string") { - if (strToValidate === undefined) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `'${strToValidate}' does not meet equals:'${stringValidation.equals}'`; - } - schema.const = stringValidation.equals; - } - if ( - stringValidation.enum && - stringValidation.enum.length > 0 && - typeof stringValidation.enum[0] === "string" - ) - schema.enum = stringValidation.enum; - if (stringValidation.minLength) schema.minLength = stringValidation.minLength; - if (stringValidation.maxLength) schema.maxLength = stringValidation.maxLength; - if (stringValidation.pattern) schema.pattern = stringValidation.pattern; - if (Object.keys(schema).length > 0) { - const validateResult = jsonschema.validate(strToValidate, schema); - if (validateResult.errors && validateResult.errors.length > 0) { - return `'${strToValidate}' ${validateResult.errors[0].message}`; - } - } - - if (stringValidation.startsWith) { - if (!strToValidate.startsWith(stringValidation.startsWith)) { - return `'${strToValidate}' does not meet startsWith:'${stringValidation.startsWith}'`; - } - } - if (stringValidation.endsWith) { - if (!strToValidate.endsWith(stringValidation.endsWith)) { - return `'${strToValidate}' does not meet endsWith:'${stringValidation.endsWith}'`; - } - } - if (stringValidation.includes) { - if (!strToValidate.includes(stringValidation.includes)) { - return `'${strToValidate}' does not meet includes:'${stringValidation.includes}'`; - } - } - if (stringValidation.notEquals) { - if (strToValidate === stringValidation.notEquals) { - return `'${strToValidate}' does not meet notEquals:'${stringValidation.notEquals}'`; - } - } - if (stringValidation.excludesEnum) { - if (stringValidation.excludesEnum.includes(strToValidate)) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `'${strToValidate}' does not meet excludesEnum:'${stringValidation.excludesEnum}'`; - } - } - } - } - - //StringArrayValidation - { - const stringArrayValidation: StringArrayValidation = validSchema as StringArrayValidation; - const arrayToValidate = value as string[]; - if (arrayToValidate === undefined || arrayToValidate instanceof Array) { - const schema: any = {}; - if (stringArrayValidation.maxItems) schema.maxItems = stringArrayValidation.maxItems; - if (stringArrayValidation.minItems) schema.minItems = stringArrayValidation.minItems; - if (stringArrayValidation.uniqueItems) schema.uniqueItems = stringArrayValidation.uniqueItems; - if (Object.keys(schema).length > 0) { - const validateResult = jsonschema.validate(arrayToValidate, schema); - if (validateResult.errors && validateResult.errors.length > 0) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `'${arrayToValidate}' ${validateResult.errors[0].message}`; - } - } - if (stringArrayValidation.equals) { - if (stringArrayValidation.equals instanceof Array) { - stringArrayValidation.enum = stringArrayValidation.equals; - stringArrayValidation.containsAll = stringArrayValidation.equals; - } else { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `'${arrayToValidate}' does not equals to:'${stringArrayValidation.equals}'`; - } - } - if (stringArrayValidation.enum && arrayToValidate) { - for (const item of arrayToValidate) { - if (!stringArrayValidation.enum.includes(item)) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `'${arrayToValidate}' does not meet with enum:'${stringArrayValidation.enum}'`; - } - } - } - if (stringArrayValidation.excludes) { - if (arrayToValidate && arrayToValidate.includes(stringArrayValidation.excludes)) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `'${arrayToValidate}' does not meet with excludes:'${stringArrayValidation.excludes}'`; - } - } - if (stringArrayValidation.contains) { - if (arrayToValidate && !arrayToValidate.includes(stringArrayValidation.contains)) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `'${arrayToValidate}' does not meet with contains:'${stringArrayValidation.contains}'`; - } - } - if (stringArrayValidation.containsAll) { - const containsAll: string[] = stringArrayValidation.containsAll; - if (containsAll.length > 0) { - for (const i of containsAll) { - if (arrayToValidate && !arrayToValidate.includes(i)) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `'${arrayToValidate}' does not meet with containsAll:'${containsAll}'`; - } - } - } - } - if (stringArrayValidation.containsAny) { - const containsAny: string[] = stringArrayValidation.containsAny; - if (containsAny.length > 0) { - // let array = valueToValidate as string[]; - let found = false; - for (const i of containsAny) { - if (arrayToValidate && arrayToValidate.includes(i)) { - found = true; - break; - } - } - if (!found) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `'${arrayToValidate}' does not meet containsAny:'${containsAny}'`; - } - } - } - } - } - return undefined; -} diff --git a/packages/api/tests/login.test.ts b/packages/api/tests/login.test.ts index 7a3aaff468..2cb6db06d0 100644 --- a/packages/api/tests/login.test.ts +++ b/packages/api/tests/login.test.ts @@ -2,56 +2,11 @@ // Licensed under the MIT license. "use strict"; -import "mocha"; -import { - AzureAccountProvider, - BasicLogin, - LoginStatus, - M365TokenProvider, - SubscriptionInfo, - TokenRequest, -} from "../src/utils/login"; import { assert } from "chai"; -import { TokenCredential } from "@azure/core-auth"; -import { ok, Result } from "neverthrow"; +import "mocha"; +import { Result, ok } from "neverthrow"; import { FxError } from "../src/error"; - -class TestAzureAccountProvider implements AzureAccountProvider { - getIdentityCredentialAsync(): Promise { - throw new Error("getIdentityCredentialAsync Method not implemented."); - } - signout(): Promise { - throw new Error("Method not implemented."); - } - setStatusChangeMap( - name: string, - statusChange: ( - status: string, - token?: string, - accountInfo?: Record - ) => Promise - ): Promise { - throw new Error("Method not implemented."); - } - removeStatusChangeMap(name: string): Promise { - throw new Error("Method not implemented."); - } - getJsonObject(showDialog?: boolean): Promise> { - throw new Error("Method not implemented."); - } - listSubscriptions(): Promise { - throw new Error("Method not implemented."); - } - setSubscription(subscriptionId: string): Promise { - throw new Error("Method not implemented."); - } - getAccountInfo(): Record { - throw new Error("Method not implemented."); - } - getSelectedSubscription(): Promise { - throw new Error("Method not implemented."); - } -} +import { BasicLogin, LoginStatus, M365TokenProvider, TokenRequest } from "../src/utils/login"; class M365Provider extends BasicLogin implements M365TokenProvider { async getAccessToken(tokenRequest: TokenRequest): Promise> { diff --git a/packages/api/tests/qm.validation.test.ts b/packages/api/tests/qm.validation.test.ts deleted file mode 100644 index b6ebb4b2d9..0000000000 --- a/packages/api/tests/qm.validation.test.ts +++ /dev/null @@ -1,411 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import * as chai from "chai"; -import "mocha"; -import { - Inputs, - Platform, - QTreeNode, - StringArrayValidation, - StringValidation, - VsCodeEnv, -} from "../src/index"; -import { FuncValidation, validate } from "../src/qm/validation"; - -describe("Question Model - Validation Test", () => { - const inputs: Inputs = { - platform: Platform.VSCode, - vscodeEnv: VsCodeEnv.local, - }; - describe("StringValidation", () => { - it("equals", async () => { - const validation: StringValidation = { equals: "123" }; - const value1 = "123"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = "1234"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 !== undefined); - const value3 = ""; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 !== undefined); - const value4 = undefined; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - }); - - it("notEquals", async () => { - const validation: StringValidation = { notEquals: "123" }; - const value1 = "123"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 !== undefined); - const value2 = "1234"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 === undefined); - const value3 = ""; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 === undefined); - const value4 = undefined; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 === undefined); - }); - - it("minLength,maxLength", async () => { - const validation: StringValidation = { minLength: 2, maxLength: 5 }; - const value1 = "a"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 !== undefined); - const value2 = "aa"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 === undefined); - const value3 = "aaaa"; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 === undefined); - const value4 = "aaaaaa"; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - const value5 = undefined; - const res5 = await validate(validation, value5, inputs); - chai.assert.isTrue(res5 !== undefined); - }); - - it("enum", async () => { - const validation: StringValidation = { enum: ["1", "2", "3"] }; - const value1 = "1"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = "3"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 === undefined); - const value3 = "4"; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 !== undefined); - const value4 = undefined; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - }); - - it("pattern", async () => { - const validation: StringValidation = { pattern: "^[0-9a-z]+$" }; - const value1 = "1"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = "asb13"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 === undefined); - const value3 = "as--123"; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 !== undefined); - const value4 = undefined; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - }); - - it("startsWith", async () => { - const validation: StringValidation = { startsWith: "123" }; - const value1 = "123"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = "234"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 !== undefined); - const value3 = "1234"; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 === undefined); - const value4 = undefined; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - }); - - it("endsWith", async () => { - const validation: StringValidation = { endsWith: "123" }; - const value1 = "123"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = "234"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 !== undefined); - const value3 = "345sdf123"; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 === undefined); - const value4 = undefined; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - }); - - it("startsWith,endsWith", async () => { - const validation: StringValidation = { startsWith: "123", endsWith: "789" }; - const value1 = "123"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 !== undefined); - const value2 = "123asfsdwer7892345789"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 === undefined); - const value3 = "sadfws789"; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 !== undefined); - const value4 = undefined; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - }); - - it("includes", async () => { - const validation: StringValidation = { includes: "123" }; - const value1 = "123"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = "123asfsdwer7892345789"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 === undefined); - const value3 = "sadfws789"; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 !== undefined); - const value4 = undefined; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - }); - - it("includes,startsWith,endsWith", async () => { - const validation: StringValidation = { startsWith: "123", endsWith: "789", includes: "abc" }; - const value1 = "123"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 !== undefined); - const value2 = "123asfabcer7892345789"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 === undefined); - const value3 = "123sadfws789"; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 !== undefined); - const value4 = "abc789"; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - const value5 = undefined; - const res5 = await validate(validation, value5, inputs); - chai.assert.isTrue(res5 !== undefined); - }); - - it("includes,startsWith,endsWith,maxLength", async () => { - const validation: StringValidation = { - startsWith: "123", - endsWith: "789", - includes: "abc", - maxLength: 10, - }; - const value1 = "123"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 !== undefined); - const value2 = "123asfabcer7892345789"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 !== undefined); - const value3 = "123sadfws789"; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 !== undefined); - const value4 = "123abch789"; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 === undefined); - }); - - it("excludesEnum", async () => { - const validation: StringValidation = { excludesEnum: ["1", "2", "3"] }; - const value1 = "1"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 !== undefined); - const value2 = "3"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 !== undefined); - const value3 = "4"; - const res3 = await validate(validation, value3, inputs); - chai.assert.isUndefined(res3); - const value4 = undefined; - const res4 = await validate(validation, value4, inputs); - chai.assert.isUndefined(res4); - }); - }); - - describe("StringArrayValidation", () => { - it("maxItems,minItems", async () => { - const validation: StringArrayValidation = { maxItems: 3, minItems: 1 }; - const value1 = ["1", "2", "3"]; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = ["1", "2", "3", "4"]; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 !== undefined); - const value3 = ["1", "2"]; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 === undefined); - const value4: string[] = []; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - const value5 = undefined; - const res5 = await validate(validation, value5, inputs); - chai.assert.isTrue(res5 !== undefined); - }); - - it("uniqueItems", async () => { - const validation: StringArrayValidation = { uniqueItems: true }; - const value1 = ["1", "2", "3"]; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = ["1", "2", "1", "2"]; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 !== undefined); - const value3 = ["1", "2"]; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 === undefined); - const value4: string[] = []; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 === undefined); - const value5 = undefined; - const res5 = await validate(validation, value5, inputs); - chai.assert.isTrue(res5 !== undefined); - }); - - it("equals", async () => { - const validation: StringArrayValidation = { equals: ["1", "2", "3"] }; - const value1 = ["1", "2", "3"]; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = ["1", "2", "1", "2"]; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 !== undefined); - const value3 = ["1", "2"]; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 !== undefined); - const value4: string[] = []; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - const value5 = undefined; - const res5 = await validate(validation, value5, inputs); - chai.assert.isTrue(res5 !== undefined); - }); - - it("enum", async () => { - const validation: StringArrayValidation = { enum: ["1", "2", "3"] }; - const value1 = ["1", "2", "3"]; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = ["1", "2", "4", "2"]; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 !== undefined); - const value3 = ["1", "2"]; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 === undefined); - const value4: string[] = []; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 === undefined); - const value5 = undefined; - const res5 = await validate(validation, value5, inputs); - chai.assert.isTrue(res5 !== undefined); - }); - - it("contains", async () => { - const validation: StringArrayValidation = { contains: "4" }; - const value1 = ["1", "2", "3"]; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 !== undefined); - const value2 = ["1", "2", "4", "2"]; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 === undefined); - const value3 = ["1", "2"]; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 !== undefined); - const value4: string[] = []; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - const value5 = undefined; - const res5 = await validate(validation, value5, inputs); - chai.assert.isTrue(res5 !== undefined); - }); - - it("containsAll", async () => { - const validation: StringArrayValidation = { containsAll: ["1", "2"] }; - const value1 = ["1", "2", "3"]; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = ["1", "2", "4", "2"]; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 === undefined); - const value3 = ["1", "3"]; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 !== undefined); - const value4: string[] = []; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - const value5 = undefined; - const res5 = await validate(validation, value5, inputs); - chai.assert.isTrue(res5 !== undefined); - }); - - it("containsAny", async () => { - const validation: StringArrayValidation = { containsAny: ["1", "2"] }; - const value1 = ["1", "2", "3"]; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 === undefined); - const value2 = ["4", "5", "6", "1"]; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 === undefined); - const value3 = ["5", "7"]; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 !== undefined); - const value4: string[] = []; - const res4 = await validate(validation, value4, inputs); - chai.assert.isTrue(res4 !== undefined); - const value5 = undefined; - const res5 = await validate(validation, value5, inputs); - chai.assert.isTrue(res5 !== undefined); - }); - }); - - it("FuncValidation", async () => { - const validation: FuncValidation = { - validFunc: function (input: string): string | undefined | Promise { - if ((input as string).length > 5) return "length > 5"; - return undefined; - }, - }; - const value1 = "123456"; - const res1 = await validate(validation, value1, inputs); - chai.assert.isTrue(res1 !== undefined); - const value2 = "12345"; - const res2 = await validate(validation, value2, inputs); - chai.assert.isTrue(res2 === undefined); - const value3 = ""; - const res3 = await validate(validation, value3, inputs); - chai.assert.isTrue(res3 === undefined); - }); - it("FuncValidation 2", async () => { - const validation = (inputs: Inputs) => { - const input = inputs.input as string; - return input.length <= 5; - }; - const inputs: Inputs = { - platform: Platform.VSCode, - }; - inputs.input = "123456"; - const res1 = await validate(validation, "", inputs); - chai.assert.isTrue(res1 !== undefined); - inputs.input = "12345"; - const res2 = await validate(validation, "", inputs); - chai.assert.isTrue(res2 === undefined); - inputs.input = ""; - const res3 = await validate(validation, "", inputs); - chai.assert.isTrue(res3 === undefined); - }); -}); - -describe("Question Model - QTreeNode", () => { - it("QTreeNode", async () => { - const node = new QTreeNode({ - type: "group", - }); - const child1 = new QTreeNode({ type: "group" }); - node.addChild(child1); - const child2 = new QTreeNode({ type: "text", name: "name", title: "title" }); - child1.addChild(child2); - node.trim(); - chai.assert.isTrue(node.children!.length === 1); - chai.assert.isTrue(node.children![0]!.data!.name === "name"); - }); -}); diff --git a/packages/api/tests/ui.test.ts b/packages/api/tests/ui.test.ts deleted file mode 100644 index 5ad80951ed..0000000000 --- a/packages/api/tests/ui.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import "mocha"; -import { Result } from "neverthrow"; -import { Colors, FxError } from "../src"; -import { - InputTextConfig, - InputTextResult, - IProgressHandler, - MultiSelectConfig, - MultiSelectResult, - SelectFileConfig, - SelectFileResult, - SelectFilesConfig, - SelectFilesResult, - SelectFolderConfig, - SelectFolderResult, - SingleSelectConfig, - SingleSelectResult, - UserInteraction, -} from "../src/qm/ui"; - -async function sleep(ms: number) { - await new Promise((resolve) => setTimeout(resolve, ms)); -} -class MockUserInteraction implements UserInteraction { - selectOption(config: SingleSelectConfig): Promise> { - throw new Error("Method not implemented."); - } - selectOptions(config: MultiSelectConfig): Promise> { - throw new Error("Method not implemented."); - } - inputText(config: InputTextConfig): Promise> { - throw new Error("Method not implemented."); - } - selectFile(config: SelectFileConfig): Promise> { - throw new Error("Method not implemented."); - } - selectFiles(config: SelectFilesConfig): Promise> { - throw new Error("Method not implemented."); - } - selectFolder(config: SelectFolderConfig): Promise> { - throw new Error("Method not implemented."); - } - - openUrl(link: string): Promise> { - throw new Error("Method not implemented."); - } - async showMessage( - level: "info" | "warn" | "error", - message: string, - modal: boolean, - ...items: string[] - ): Promise>; - - async showMessage( - level: "info" | "warn" | "error", - message: Array<{ content: string; color: Colors }>, - modal: boolean, - ...items: string[] - ): Promise>; - - async showMessage( - level: "info" | "warn" | "error", - message: string | Array<{ content: string; color: Colors }>, - modal: boolean, - ...items: string[] - ): Promise> { - throw new Error("Method not implemented."); - } - createProgressBar(title: string, totalSteps: number): IProgressHandler { - throw new Error("Method not implemented."); - } -} diff --git a/packages/cli/.vscode/launch.json b/packages/cli/.vscode/launch.json index 7c725154a6..4338fe3e8e 100644 --- a/packages/cli/.vscode/launch.json +++ b/packages/cli/.vscode/launch.json @@ -10,7 +10,7 @@ "name": "Launch new command (non-interactive)", "skipFiles": ["/**"], "program": "${workspaceFolder}/cli.js", - "args": ["config", "set", "interactive", "false"], + "args": ["new", "-i", "false"], "outFiles": [ "${workspaceFolder}/lib/**/*.js", "${workspaceFolder}/../fx-core/build/**/*.js", diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 7209f61f2f..be423be87a 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { Inputs, Platform, QTreeNode, Stage } from "@microsoft/teamsfx-api"; +import { IQTreeNode, Inputs, Platform, Stage } from "@microsoft/teamsfx-api"; import { CoreQuestionNames } from "@microsoft/teamsfx-core"; -import { Options } from "yargs"; import chalk from "chalk"; +import { Options } from "yargs"; export type OptionsMap = { [_: string]: Options }; @@ -14,12 +14,14 @@ export const cliTelemetryPrefix = "teamsfx-cli"; export const teamsAppFileName = "teamsapp.yml"; -export const RootFolderNode = new QTreeNode({ - type: "folder", - name: "folder", - title: "Select root folder of the project", - default: "./", -}); +export const RootFolderNode: IQTreeNode = { + data: { + type: "folder", + name: "folder", + title: "Select root folder of the project", + default: "./", + }, +}; export const RootFolderOptions: OptionsMap = { folder: { @@ -30,11 +32,13 @@ export const RootFolderOptions: OptionsMap = { }, }; -export const EnvNodeNoCreate = new QTreeNode({ - type: "text", - name: "env", - title: "Select an existing environment for the project", -}); +export const EnvNodeNoCreate: IQTreeNode = { + data: { + type: "text", + name: "env", + title: "Select an existing environment for the project", + }, +}; export const EnvOptions: OptionsMap = { env: { @@ -58,17 +62,21 @@ export const ProvisionOptions: OptionsMap = { }, }; -export const SubscriptionNode = new QTreeNode({ - type: "text", - name: "subscription", - title: "Select a subscription", -}); +export const SubscriptionNode: IQTreeNode = { + data: { + type: "text", + name: "subscription", + title: "Select a subscription", + }, +}; -export const CollaboratorEmailNode = new QTreeNode({ - type: "text", - name: "email", - title: "Input email address of collaborator", -}); +export const CollaboratorEmailNode: IQTreeNode = { + data: { + type: "text", + name: "email", + title: "Input email address of collaborator", + }, +}; export const CollaboratorEmailOptions: OptionsMap = { email: { @@ -161,7 +169,7 @@ export const AddFeatureFunc = { method: Stage.addFeature, }; -export const EmptyQTreeNode = new QTreeNode({ type: "group" }); +export const EmptyQTreeNode: IQTreeNode = { data: { type: "group" } }; export const SUPPORTED_SPFX_VERSION = "1.16.1"; diff --git a/packages/cli/src/questionUtils.ts b/packages/cli/src/questionUtils.ts index ad8c85efcc..a9da88b125 100644 --- a/packages/cli/src/questionUtils.ts +++ b/packages/cli/src/questionUtils.ts @@ -5,25 +5,24 @@ import { IQTreeNode, MultiSelectQuestion, OptionItem, - QTreeNode, Question, SingleSelectQuestion, StaticOptions, - validate, } from "@microsoft/teamsfx-api"; import { isAutoSkipSelect } from "@microsoft/teamsfx-core"; import { Options } from "yargs"; import { getSingleOptionString, toYargsOptions } from "./utils"; import { globals } from "./globals"; +import { validate } from "@microsoft/teamsfx-core"; export async function filterQTreeNode( - root: QTreeNode, + root: IQTreeNode, key: string, value: any -): Promise { +): Promise { /// finds the searched node - let searchedNode: QTreeNode | undefined = undefined; - const parentMap = new Map(); + let searchedNode: IQTreeNode | undefined = undefined; + const parentMap = new Map(); const stack = [root]; while (stack.length > 0) { const currentNode = stack.pop(); @@ -49,7 +48,7 @@ export async function filterQTreeNode( (searchedNode.data as any).hide = true; /// gets the children which conditions match the parent's answer - const matchedChildren: QTreeNode[] = []; + const matchedChildren: IQTreeNode[] = []; if (searchedNode.children) { for (const child of searchedNode.children) { if (child && child.condition) { diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index e0a090c9d9..5b006f83fc 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -8,7 +8,6 @@ import { MultiSelectQuestion, OptionItem, Platform, - QTreeNode, Question, SingleSelectQuestion, } from "@microsoft/teamsfx-api"; diff --git a/packages/cli/tests/unit/questionUtils.tests.ts b/packages/cli/tests/unit/questionUtils.tests.ts index a7acdccf52..6a315b8d7f 100644 --- a/packages/cli/tests/unit/questionUtils.tests.ts +++ b/packages/cli/tests/unit/questionUtils.tests.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { QTreeNode, Question, StaticOptions } from "@microsoft/teamsfx-api"; +import { IQTreeNode, Question, StaticOptions } from "@microsoft/teamsfx-api"; import "mocha"; import sinon from "sinon"; import { filterQTreeNode } from "../../src/questionUtils"; @@ -21,23 +21,28 @@ describe("Question Utils Tests", function () { cliName: "azure-sql", }, ]; - const root = new QTreeNode({ - type: "multiSelect", - name: "add-azure-resources", - title: "Cloud Resources", - staticOptions: resources, - onDidChangeSelection: async (currentSelectedIds: Set, _: any) => { - if (currentSelectedIds.has("sql")) currentSelectedIds.add("function"); - return currentSelectedIds; + const root: IQTreeNode = { + data: { + type: "multiSelect", + name: "add-azure-resources", + title: "Cloud Resources", + staticOptions: resources, + onDidChangeSelection: async (currentSelectedIds: Set, _: any) => { + if (currentSelectedIds.has("sql")) currentSelectedIds.add("function"); + return currentSelectedIds; + }, }, - }); - const functionNode = new QTreeNode({ - type: "text", - name: "function-name", - title: "Function Name", - }); - functionNode.condition = { contains: "function" }; - root.addChild(functionNode); + children: [ + { + condition: { contains: "function" }, + data: { + type: "text", + name: "function-name", + title: "Function Name", + }, + }, + ], + }; afterEach(() => { sandbox.restore(); diff --git a/packages/cli/tests/unit/utils.tests.ts b/packages/cli/tests/unit/utils.tests.ts index 93e0e7b1ad..3a672e45a0 100644 --- a/packages/cli/tests/unit/utils.tests.ts +++ b/packages/cli/tests/unit/utils.tests.ts @@ -2,11 +2,14 @@ // Licensed under the MIT license. import * as apis from "@microsoft/teamsfx-api"; +import { Colors, IQTreeNode, Platform } from "@microsoft/teamsfx-api"; import * as core from "@microsoft/teamsfx-core"; -import { Colors, Platform, QTreeNode } from "@microsoft/teamsfx-api"; import fs from "fs-extra"; import "mocha"; import sinon from "sinon"; +import activate from "../../src/activate"; +import AzureAccountManager from "../../src/commonlib/azureLogin"; +import { UserSettings } from "../../src/userSetttings"; import { flattenNodes, getColorizedString, @@ -19,9 +22,6 @@ import { toYargsOptions, } from "../../src/utils"; import { expect } from "./utils"; -import { UserSettings } from "../../src/userSetttings"; -import AzureAccountManager from "../../src/commonlib/azureLogin"; -import activate from "../../src/activate"; const staticOptions1: apis.StaticOptions = ["a", "b", "c"]; const staticOptions2: apis.StaticOptions = [ @@ -158,13 +158,19 @@ describe("Utils Tests", function () { }); it("flattenNodes", () => { - const root = new QTreeNode({ - type: "group", - }); - root.children = [ - new QTreeNode({ type: "folder", name: "a", title: "aa" }), - new QTreeNode({ type: "folder", name: "b", title: "bb" }), - ]; + const root: IQTreeNode = { + data: { + type: "group", + }, + children: [ + { + data: { type: "folder", name: "a", title: "aa" }, + }, + { + data: { type: "folder", name: "b", title: "bb" }, + }, + ], + }; const answers = flattenNodes(root); expect(answers.map((a) => a.data)).deep.equals([ { type: "group" }, diff --git a/packages/fx-core/src/component/middleware/actionExecutionMW.ts b/packages/fx-core/src/component/middleware/actionExecutionMW.ts index fe55921be7..be5d0f3163 100644 --- a/packages/fx-core/src/component/middleware/actionExecutionMW.ts +++ b/packages/fx-core/src/component/middleware/actionExecutionMW.ts @@ -8,7 +8,7 @@ import { IProgressHandler, InputsWithProjectPath, MaybePromise, - QTreeNode, + IQTreeNode, Result, SystemError, UserError, @@ -38,7 +38,7 @@ interface ActionOption { question?: ( context: Context, inputs: InputsWithProjectPath - ) => MaybePromise>; + ) => MaybePromise>; } export interface ActionContext { progressBar?: IProgressHandler; diff --git a/packages/fx-core/src/core/FxCore.ts b/packages/fx-core/src/core/FxCore.ts index fb535a94ce..cb68ea860e 100644 --- a/packages/fx-core/src/core/FxCore.ts +++ b/packages/fx-core/src/core/FxCore.ts @@ -16,10 +16,10 @@ import { FxError, Inputs, InputsWithProjectPath, + IQTreeNode, ok, OpenAIPluginManifest, Platform, - QTreeNode, Result, Stage, Tools, @@ -565,9 +565,9 @@ export class FxCore { /** * Warning: this API only works for CLI_HELP, it has no business with interactive run for CLI! */ - getQuestions(stage: Stage, inputs: Inputs): Result { + getQuestions(stage: Stage, inputs: Inputs): Result { if (stage === Stage.create) { - return ok(createProjectCliHelpNode() as QTreeNode); + return ok(createProjectCliHelpNode()); } return ok(undefined); } diff --git a/packages/fx-core/src/index.ts b/packages/fx-core/src/index.ts index 8b3dbc49a4..45da641ce9 100644 --- a/packages/fx-core/src/index.ts +++ b/packages/fx-core/src/index.ts @@ -39,5 +39,6 @@ export { QuestionNames as CoreQuestionNames } from "./question/questionNames"; export * from "./core/types"; export * from "./error/index"; export * from "./ui/visitor"; +export * from "./ui/validationUtils"; export * from "./question"; export * from "./component/generator/copilotPlugin/helper"; diff --git a/packages/fx-core/src/question/generator.ts b/packages/fx-core/src/question/generator.ts index 1f4ef95060..408c0da4e1 100644 --- a/packages/fx-core/src/question/generator.ts +++ b/packages/fx-core/src/question/generator.ts @@ -12,7 +12,6 @@ import { Platform, SingleSelectQuestion, UserInputQuestion, - validate, } from "@microsoft/teamsfx-api"; import path from "path"; import { @@ -24,6 +23,7 @@ import { VariableDeclarationKind, } from "ts-morph"; import { questionNodes } from "."; +import { validate } from "../ui/validationUtils"; async function collectNodesForCliOptions(node: IQTreeNode, nodeList: IQTreeNode[]) { if (node.cliOptionDisabled === "all") return; diff --git a/packages/fx-core/src/question/other.ts b/packages/fx-core/src/question/other.ts index ba46f55ecd..47dc846ef0 100644 --- a/packages/fx-core/src/question/other.ts +++ b/packages/fx-core/src/question/other.ts @@ -571,6 +571,7 @@ function selectAppTypeQuestion(): MultiSelectQuestion { }, ], validation: { minItems: 1 }, + validationHelp: "Please select at least one app type.", }; } diff --git a/packages/fx-core/src/ui/validationUtils.ts b/packages/fx-core/src/ui/validationUtils.ts index bf214818bd..4c3e1fd8af 100644 --- a/packages/fx-core/src/ui/validationUtils.ts +++ b/packages/fx-core/src/ui/validationUtils.ts @@ -2,16 +2,21 @@ // Licensed under the MIT license. import { + ConditionFunc, + FuncValidation, Inputs, MultiSelectQuestion, OptionItem, Question, SingleSelectQuestion, StaticOptions, + StaticValidation, + StringArrayValidation, + StringValidation, ValidationSchema, - getValidationFunction, } from "@microsoft/teamsfx-api"; import { EmptyOptionError } from "../error/common"; +import * as jsonschema from "jsonschema"; class ValidationUtils { async validateInputForSingleSelectQuestion( @@ -125,3 +130,212 @@ class ValidationUtils { } export const validationUtils = new ValidationUtils(); + +/** + * A function to return a validation function according the validation schema + * @param validation validation schema + * @param inputs object to carry all user inputs + * @returns a validation function + */ +export function getValidationFunction( + validation: ValidationSchema, + inputs: Inputs +): (input: T) => string | undefined | Promise { + return function (input: T): string | undefined | Promise { + return validate(validation, input, inputs); + }; +} +/** + * Implementation of validation function + * @param validSchema validation schema + * @param value value to validate + * @param inputs user inputs object, which works as the context of the validation + * @returns A human-readable string which is presented as diagnostic message. + * Return `undefined` when 'value' is valid. + */ +export async function validate( + validSchema: ValidationSchema | ConditionFunc, + value: T, + inputs?: Inputs +): Promise { + { + //FuncValidation + const funcValidation: FuncValidation = validSchema as FuncValidation; + if (funcValidation.validFunc) { + const res = await funcValidation.validFunc(value, inputs); + return res as string; + } else if (typeof funcValidation === "function") { + const res = await (funcValidation as ConditionFunc)(inputs!); + if (res) return undefined; + return "condition function is not met."; + } + } + + if (!value) { + if ((validSchema as StaticValidation).required === true) return `input value is required.`; + } + + const noneEmptyKeyNum = Object.keys(validSchema).filter( + (key) => (validSchema as any)[key] !== undefined + ).length; + + if (noneEmptyKeyNum === 0) { + return undefined; + } + + if ( + value === undefined && + ((validSchema as any).required || + (validSchema as any).equals || + (validSchema as any).maxLength || + (validSchema as any).minLength || + (validSchema as any).pattern || + (validSchema as any).enum || + (validSchema as any).startsWith || + (validSchema as any).endsWith || + (validSchema as any).includes || + (validSchema as any).maxItems || + (validSchema as any).minItems || + (validSchema as any).uniqueItems || + (validSchema as any).contains || + (validSchema as any).containsAll || + (validSchema as any).containsAny) + ) { + return `'undefined' does not meet condition:'${JSON.stringify(validSchema)}'`; + } + const jsonValue = JSON.stringify(value); + { + // StringValidation + const stringValidation: StringValidation = validSchema as StringValidation; + const strToValidate = value as string; + if (strToValidate === undefined || typeof strToValidate === "string") { + const schema: any = {}; + if (stringValidation.equals && typeof stringValidation.equals === "string") { + if (strToValidate === undefined) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${jsonValue} does not meet equals:'${stringValidation.equals}'`; + } + schema.const = stringValidation.equals; + } + if ( + stringValidation.enum && + stringValidation.enum.length > 0 && + typeof stringValidation.enum[0] === "string" + ) + schema.enum = stringValidation.enum; + if (stringValidation.minLength) schema.minLength = stringValidation.minLength; + if (stringValidation.maxLength) schema.maxLength = stringValidation.maxLength; + if (stringValidation.pattern) schema.pattern = stringValidation.pattern; + if (Object.keys(schema).length > 0) { + const validateResult = jsonschema.validate(strToValidate, schema); + if (validateResult.errors && validateResult.errors.length > 0) { + return `${jsonValue} ${validateResult.errors[0].message}`; + } + } + + if (stringValidation.startsWith) { + if (!strToValidate.startsWith(stringValidation.startsWith)) { + return `${jsonValue} does not meet startsWith:'${stringValidation.startsWith}'`; + } + } + if (stringValidation.endsWith) { + if (!strToValidate.endsWith(stringValidation.endsWith)) { + return `${jsonValue} does not meet endsWith:'${stringValidation.endsWith}'`; + } + } + if (stringValidation.includes) { + if (!strToValidate.includes(stringValidation.includes)) { + return `${jsonValue} does not meet includes:'${stringValidation.includes}'`; + } + } + if (stringValidation.notEquals) { + if (strToValidate === stringValidation.notEquals) { + return `${jsonValue} does not meet notEquals:'${stringValidation.notEquals}'`; + } + } + if (stringValidation.excludesEnum) { + if (stringValidation.excludesEnum.includes(strToValidate)) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${jsonValue} does not meet excludesEnum:'${stringValidation.excludesEnum}'`; + } + } + } + } + + //StringArrayValidation + { + const stringArrayValidation: StringArrayValidation = validSchema as StringArrayValidation; + const arrayToValidate = value as string[]; + if (arrayToValidate === undefined || arrayToValidate instanceof Array) { + const schema: any = {}; + if (stringArrayValidation.maxItems) schema.maxItems = stringArrayValidation.maxItems; + if (stringArrayValidation.minItems) schema.minItems = stringArrayValidation.minItems; + if (stringArrayValidation.uniqueItems) schema.uniqueItems = stringArrayValidation.uniqueItems; + if (Object.keys(schema).length > 0) { + const validateResult = jsonschema.validate(arrayToValidate, schema); + if (validateResult.errors && validateResult.errors.length > 0) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${jsonValue} ${validateResult.errors[0].message}`; + } + } + if (stringArrayValidation.equals) { + if (stringArrayValidation.equals instanceof Array) { + stringArrayValidation.enum = stringArrayValidation.equals; + stringArrayValidation.containsAll = stringArrayValidation.equals; + } else { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${jsonValue} does not equals to:'${stringArrayValidation.equals}'`; + } + } + if (stringArrayValidation.enum && arrayToValidate) { + for (const item of arrayToValidate) { + if (!stringArrayValidation.enum.includes(item)) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${jsonValue} does not meet with enum:'${stringArrayValidation.enum}'`; + } + } + } + if (stringArrayValidation.excludes) { + if (arrayToValidate && arrayToValidate.includes(stringArrayValidation.excludes)) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${jsonValue} does not meet with excludes:'${stringArrayValidation.excludes}'`; + } + } + if (stringArrayValidation.contains) { + if (arrayToValidate && !arrayToValidate.includes(stringArrayValidation.contains)) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${jsonValue} does not meet with contains:'${stringArrayValidation.contains}'`; + } + } + if (stringArrayValidation.containsAll) { + const containsAll: string[] = stringArrayValidation.containsAll; + if (containsAll.length > 0) { + for (const i of containsAll) { + if (arrayToValidate && !arrayToValidate.includes(i)) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${jsonValue} does not meet with containsAll:'${containsAll}'`; + } + } + } + } + if (stringArrayValidation.containsAny) { + const containsAny: string[] = stringArrayValidation.containsAny; + if (containsAny.length > 0) { + // let array = valueToValidate as string[]; + let found = false; + for (const i of containsAny) { + if (arrayToValidate && arrayToValidate.includes(i)) { + found = true; + break; + } + } + if (!found) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${jsonValue} does not meet containsAny:'${containsAny}'`; + } + } + } + } + } + return undefined; +} diff --git a/packages/fx-core/src/ui/visitor.ts b/packages/fx-core/src/ui/visitor.ts index 6f6be52f25..c9a6754c74 100644 --- a/packages/fx-core/src/ui/visitor.ts +++ b/packages/fx-core/src/ui/visitor.ts @@ -8,7 +8,6 @@ import { Inputs, MultiSelectQuestion, OptionItem, - QTreeNode, Question, Result, SingleSelectQuestion, @@ -20,11 +19,10 @@ import { UserInteraction, Void, err, - getValidationFunction, ok, - validate, } from "@microsoft/teamsfx-api"; import { assign, cloneDeep } from "lodash"; +import { isCliNewUxEnabled } from "../common/featureFlags"; import { EmptyOptionError, InputValidationError, @@ -32,8 +30,7 @@ import { UserCancelError, assembleError, } from "../error"; -import { validationUtils } from "./validationUtils"; -import { isCliNewUxEnabled } from "../common/featureFlags"; +import { getValidationFunction, validate, validationUtils } from "./validationUtils"; export function isAutoSkipSelect(q: Question): boolean { if (q.type === "singleSelect" || q.type === "multiSelect") { @@ -455,10 +452,7 @@ export async function traverse( return ok(Void); } -function findValue( - curr: QTreeNode | IQTreeNode, - parentMap: Map -): any { +function findValue(curr: IQTreeNode, parentMap: Map): any { if (curr.data.type !== "group") { // need to convert OptionItem value into id for validation if (curr.data.type === "singleSelect") { diff --git a/packages/fx-core/tests/question/question.spfx.test.ts b/packages/fx-core/tests/question/question.spfx.test.ts index b15be130c1..d575840088 100644 --- a/packages/fx-core/tests/question/question.spfx.test.ts +++ b/packages/fx-core/tests/question/question.spfx.test.ts @@ -15,12 +15,12 @@ import { SingleSelectQuestion, Stage, TextInputQuestion, - getValidationFunction, } from "@microsoft/teamsfx-api"; import { getLocalizedString } from "../../src/common/localizeUtils"; import * as path from "path"; import fs from "fs-extra"; import { Utils } from "../../src/component/generator/spfx/utils/utils"; +import { getValidationFunction } from "../../src/ui/validationUtils"; describe("SPFx question-helpers", () => { describe("SPFxWebpartNameQuestion", () => { let mockedEnvRestore: RestoreFn; diff --git a/packages/fx-core/tests/ui/qm.visitor.test.ts b/packages/fx-core/tests/ui/qm.visitor.test.ts index 31defcc468..2b677efa75 100644 --- a/packages/fx-core/tests/ui/qm.visitor.test.ts +++ b/packages/fx-core/tests/ui/qm.visitor.test.ts @@ -1,31 +1,32 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { assert } from "chai"; -import "mocha"; -import sinon from "sinon"; import { Colors, + FolderQuestion, FxError, IProgressHandler, + IQTreeNode, InputResult, InputTextConfig, InputTextResult, Inputs, + MultiFileQuestion, MultiSelectConfig, MultiSelectQuestion, MultiSelectResult, OptionItem, Platform, - QTreeNode, Result, SelectFileConfig, - SingleFileOrInputQuestion, SelectFileResult, SelectFilesConfig, SelectFilesResult, SelectFolderConfig, SelectFolderResult, + SingleFileOrInputConfig, + SingleFileOrInputQuestion, + SingleFileQuestion, SingleSelectConfig, SingleSelectQuestion, SingleSelectResult, @@ -35,12 +36,12 @@ import { UserInteraction, err, ok, - SingleFileOrInputConfig, - IQTreeNode, - MultiFileQuestion, - SingleFileQuestion, - FolderQuestion, } from "@microsoft/teamsfx-api"; +import { assert } from "chai"; +import "mocha"; +import mockedEnv, { RestoreFn } from "mocked-env"; +import sinon from "sinon"; +import { setTools } from "../../src/core/globalVars"; import { EmptyOptionError, InputValidationError, @@ -48,9 +49,7 @@ import { UserCancelError, } from "../../src/error/common"; import { loadOptions, questionVisitor, traverse } from "../../src/ui/visitor"; -import mockedEnv, { RestoreFn } from "mocked-env"; import { MockTools } from "../core/utils"; -import { setTools } from "../../src/core/globalVars"; function createInputs(): Inputs { return { @@ -147,99 +146,6 @@ describe("Question Model - Visitor Test", () => { afterEach(() => { sandbox.restore(); }); - describe("question", () => { - it("trim() case 1", async () => { - const node1 = new QTreeNode({ type: "group" }); - const node2 = new QTreeNode({ type: "group" }); - const node3 = new QTreeNode({ type: "group" }); - node1.addChild(node2); - node1.addChild(node3); - const trimed = node1.trim(); - assert.isTrue(trimed === undefined); - }); - - it("trim() case 2", async () => { - const node1 = new QTreeNode({ type: "group" }); - const node2 = new QTreeNode({ type: "group" }); - const node3 = new QTreeNode({ type: "text", name: "t1", title: "t1" }); - node3.condition = { equals: "1" }; - node1.addChild(node2); - node2.addChild(node3); - const trimed = node1.trim(); - assert.isTrue(trimed && trimed.data.name === "t1" && trimed.validate()); - }); - - it("trim() case 3 - parent node has condition, and child node has no condition.", async () => { - const condition: StringValidation = { - equals: "test", - }; - - // Arrange - // input - const node1 = new QTreeNode({ type: "group" }); - node1.condition = condition; - const node2 = new QTreeNode({ type: "text", name: "t1", title: "t1" }); - node1.addChild(node2); - - // expected - const expected1 = new QTreeNode({ type: "text", name: "t1", title: "t1" }); - expected1.condition = condition; - - // Act - const trimmed = node1.trim(); - - // Assert - assert.deepEqual(trimmed, expected1); - }); - it("trim() case 4 - parent node has no condition, and child node has condition.", async () => { - const condition: StringValidation = { - equals: "test", - }; - - // Arrange - // input - const node1 = new QTreeNode({ type: "group" }); - const node2 = new QTreeNode({ type: "text", name: "t1", title: "t1" }); - node2.condition = condition; - node1.addChild(node2); - - // expected - const expected1 = new QTreeNode({ type: "text", name: "t1", title: "t1" }); - expected1.condition = condition; - - // Act - const trimmed = node1.trim(); - - // Assert - assert.deepEqual(trimmed, expected1); - }); - it("trim() case 5 - parent node has condition, and child node has condition.", async () => { - const condition: StringValidation = { - equals: "test", - }; - - // Arrange - // input - const node1 = new QTreeNode({ type: "group" }); - node1.condition = condition; - const node2 = new QTreeNode({ type: "text", name: "t1", title: "t1" }); - node2.condition = condition; - node1.addChild(node2); - - // expected - const expected1 = new QTreeNode({ type: "group" }); - expected1.condition = condition; - const expected2 = new QTreeNode({ type: "text", name: "t1", title: "t1" }); - expected2.condition = condition; - expected1.addChild(expected2); - - // Act - const trimmed = node1.trim(); - - // Assert - assert.deepEqual(trimmed, expected1); - }); - }); describe("traverse()", () => { beforeEach(() => {}); @@ -260,11 +166,14 @@ describe("Question Model - Visitor Test", () => { assert(config.step === actualStep); return ok({ type: "success", result: `mocked value of ${config.name}` }); }); - const root = new QTreeNode({ type: "group" }); + const root: IQTreeNode = { + data: { type: "group" }, + children: [], + }; const expectedSequence: string[] = []; for (let i = 1; i <= num; ++i) { - root.addChild(new QTreeNode(createTextQuestion(`${i}`))); + root.children!.push({ data: createTextQuestion(`${i}`) }); if (i < cancelNum) expectedSequence.push(`${i}`); } const inputs = createInputs(); @@ -284,11 +193,14 @@ describe("Question Model - Visitor Test", () => { assert(config.step === actualStep); return ok({ type: "success", result: `mocked value of ${config.name}` }); }); - const root = new QTreeNode({ type: "group" }); + const root: IQTreeNode = { + data: { type: "group" }, + children: [], + }; const num = 10; const expectedSequence: string[] = []; for (let i = 1; i <= num; ++i) { - root.addChild(new QTreeNode(createTextQuestion(`${i}`))); + root.children!.push({ data: createTextQuestion(`${i}`) }); expectedSequence.push(`${i}`); } const inputs = createInputs(); @@ -306,7 +218,10 @@ describe("Question Model - Visitor Test", () => { actualSequence.push(config.name); return ok({ type: "success", result: `mocked value of ${config.name}` }); }); - const root = new QTreeNode({ type: "group" }); + const root: IQTreeNode = { + data: { type: "group" }, + children: [], + }; const num = 10; const expectedSequence: string[] = []; for (let i = 1; i <= num; ++i) { @@ -318,8 +233,7 @@ describe("Question Model - Visitor Test", () => { expectedSequence.push(name); } question.skipSingleOption = true; - const current = new QTreeNode(question); - root.addChild(current); + root.children!.push({ data: question }); } const inputs = createInputs(); const res = await traverse(root, inputs, mockUI); @@ -425,28 +339,31 @@ describe("Question Model - Visitor Test", () => { }); } ); - const root = new QTreeNode({ type: "group" }); + const root: IQTreeNode = { + data: { type: "group" }, + children: [], + }; const expectedSequence: string[] = ["1", "4"]; const question1 = createSingleSelectQuestion("1"); question1.staticOptions = [{ id: `mocked value of 1`, label: `mocked value of 1` }]; question1.returnObject = true; - root.addChild(new QTreeNode(question1)); + root.children!.push({ data: question1 }); const question2 = createSingleSelectQuestion("2"); question2.staticOptions = [{ id: `mocked value of 2`, label: `mocked value of 2` }]; question2.skipSingleOption = true; - root.addChild(new QTreeNode(question2)); + root.children!.push({ data: question2 }); const question3 = createMultiSelectQuestion("3"); question3.staticOptions = [{ id: `mocked value of 3`, label: `mocked value of 3` }]; question3.skipSingleOption = true; question3.returnObject = true; - root.addChild(new QTreeNode(question3)); + root.children!.push({ data: question3 }); const question4 = createMultiSelectQuestion("4"); question4.staticOptions = [{ id: `mocked value of 4`, label: `mocked value of 4` }]; - root.addChild(new QTreeNode(question4)); + root.children!.push({ data: question4 }); const res = await traverse(root, inputs, mockUI); assert.isTrue(res.isOk()); @@ -496,21 +413,18 @@ describe("Question Model - Visitor Test", () => { const question1 = createSingleSelectQuestion("1"); question1.staticOptions = ["2", "3"]; question1.returnObject = true; - const node1 = new QTreeNode(question1); const question2 = createSingleSelectQuestion("2"); question2.staticOptions = [{ id: `mocked value of 2`, label: `mocked value of 2` }]; question2.skipSingleOption = true; - const node2 = new QTreeNode(question2); - node2.condition = { equals: "2" }; - node1.addChild(node2); + const node2: IQTreeNode = { data: question2, condition: { equals: "2" } }; const question3 = createMultiSelectQuestion("3"); question3.staticOptions = [{ id: `mocked value of 3`, label: `mocked value of 3` }]; question3.skipSingleOption = true; - const node3 = new QTreeNode(question3); - node3.condition = { equals: "3" }; - node1.addChild(node3); + const node3: IQTreeNode = { data: question3, condition: { equals: "3" } }; + + const node1: IQTreeNode = { data: question1, children: [node2, node3] }; const res = await traverse(node1, inputs, mockUI); assert.isTrue(res.isOk()); @@ -553,21 +467,17 @@ describe("Question Model - Visitor Test", () => { { id: "3", label: "3" }, ]; question1.returnObject = true; - const node1 = new QTreeNode(question1); - const question2 = createSingleSelectQuestion("2"); question2.staticOptions = [{ id: `mocked value of 2`, label: `mocked value of 2` }]; question2.skipSingleOption = true; - const node2 = new QTreeNode(question2); - node2.condition = { equals: "2" }; - node1.addChild(node2); + const node2: IQTreeNode = { data: question2, condition: { equals: "2" } }; const question3 = createMultiSelectQuestion("3"); question3.staticOptions = [{ id: `mocked value of 3`, label: `mocked value of 3` }]; question3.skipSingleOption = true; - const node3 = new QTreeNode(question3); - node3.condition = { equals: "3" }; - node1.addChild(node3); + const node3: IQTreeNode = { data: question3, condition: { equals: "3" } }; + + const node1: IQTreeNode = { data: question1, children: [node2, node3] }; const res = await traverse(node1, inputs, mockUI); assert.isTrue(res.isOk()); @@ -599,15 +509,12 @@ describe("Question Model - Visitor Test", () => { } ); - const root = new QTreeNode({ type: "group" }); - const question1 = createSingleSelectQuestion("1"); question1.staticOptions = [ { id: `mocked value of 1`, label: `mocked value of 1` }, { id: `mocked value of 2`, label: `mocked value of 2` }, ]; question1.returnObject = true; - root.addChild(new QTreeNode(question1)); inputs["1"] = { id: `mocked value of 1`, label: `mocked value of 1` }; const question3 = createMultiSelectQuestion("3"); @@ -617,8 +524,11 @@ describe("Question Model - Visitor Test", () => { ]; question3.skipSingleOption = true; question3.returnObject = true; - root.addChild(new QTreeNode(question3)); + const root: IQTreeNode = { + data: { type: "group" }, + children: [{ data: question1 }, { data: question3 }], + }; const res = await traverse(root, inputs, mockUI); assert.isTrue(res.isOk()); assert.equal((multiSelect.lastCall.args[0] as MultiSelectConfig).step, 1); @@ -640,19 +550,19 @@ describe("Question Model - Visitor Test", () => { const expectedSequence: string[] = ["1", "2", "3", "2", "3", "4"]; const question1 = createTextQuestion("1"); - const node1 = new QTreeNode(question1); - const question2 = createTextQuestion("2"); - const node2 = new QTreeNode(question2); - node1.addChild(node2); - const question3 = createTextQuestion("3"); - const node3 = new QTreeNode(question3); - node2.addChild(node3); - const question4 = createTextQuestion("4"); - const node4 = new QTreeNode(question4); - node2.addChild(node4); + + const node1: IQTreeNode = { + data: question1, + children: [ + { + data: question2, + children: [{ data: question3 }, { data: question4 }], + }, + ], + }; const res = await traverse(node1, inputs, mockUI); assert.isTrue(res.isOk()); @@ -672,7 +582,7 @@ describe("Question Model - Visitor Test", () => { dynamicOptions: () => Promise.resolve([{ id: "1", label: "1" }]), }; const inputs = createInputs(); - const res = await traverse(new QTreeNode(question), inputs, mockUI); + const res = await traverse({ data: question }, inputs, mockUI); assert.isTrue(res.isOk()); assert.isTrue(inputs["test"] === "1"); }); @@ -686,7 +596,7 @@ describe("Question Model - Visitor Test", () => { staticOptions: [], }; const inputs = createInputs(); - const res = await traverse(new QTreeNode(question), inputs, mockUI); + const res = await traverse({ data: question }, inputs, mockUI); assert.isTrue(res.isErr()); if (res.isErr()) { assert.isTrue(res.error instanceof EmptyOptionError); @@ -710,7 +620,7 @@ describe("Question Model - Visitor Test", () => { }, }; const inputs = createInputs(); - const res = await traverse(new QTreeNode(question), inputs, mockUI); + const res = await traverse({ data: question }, inputs, mockUI); assert.isTrue(res.isOk()); assert.isTrue(inputs["test"] === "file"); }); @@ -736,7 +646,7 @@ describe("Question Model - Visitor Test", () => { validation: validation, }; const inputs = createInputs(); - const res = await traverse(new QTreeNode(question), inputs, mockUI); + const res = await traverse({ data: question }, inputs, mockUI); assert.isTrue(res.isOk()); assert.isTrue(inputs["test"] === "file"); }); diff --git a/packages/fx-core/tests/ui/validationUtils.test.ts b/packages/fx-core/tests/ui/validationUtils.test.ts index c23c71b600..c651729ccf 100644 --- a/packages/fx-core/tests/ui/validationUtils.test.ts +++ b/packages/fx-core/tests/ui/validationUtils.test.ts @@ -1,11 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { Inputs, Platform } from "@microsoft/teamsfx-api"; +import { + FuncValidation, + Inputs, + Platform, + StringArrayValidation, + StringValidation, + VsCodeEnv, +} from "@microsoft/teamsfx-api"; import { assert } from "chai"; import "mocha"; import sinon from "sinon"; -import { validationUtils } from "../../src/ui/validationUtils"; +import { validate, validationUtils } from "../../src/ui/validationUtils"; describe("ValidationUtils", () => { const sandbox = sinon.createSandbox(); @@ -256,3 +263,385 @@ describe("ValidationUtils", () => { }); }); }); + +describe("validate", () => { + const inputs: Inputs = { + platform: Platform.VSCode, + vscodeEnv: VsCodeEnv.local, + }; + describe("StringValidation", () => { + it("equals", async () => { + const validation: StringValidation = { equals: "123" }; + const value1 = "123"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = "1234"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 !== undefined); + const value3 = ""; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 !== undefined); + const value4 = undefined; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + }); + + it("notEquals", async () => { + const validation: StringValidation = { notEquals: "123" }; + const value1 = "123"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 !== undefined); + const value2 = "1234"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 === undefined); + const value3 = ""; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 === undefined); + const value4 = undefined; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 === undefined); + }); + + it("minLength,maxLength", async () => { + const validation: StringValidation = { minLength: 2, maxLength: 5 }; + const value1 = "a"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 !== undefined); + const value2 = "aa"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 === undefined); + const value3 = "aaaa"; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 === undefined); + const value4 = "aaaaaa"; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + const value5 = undefined; + const res5 = await validate(validation, value5, inputs); + assert.isTrue(res5 !== undefined); + }); + + it("enum", async () => { + const validation: StringValidation = { enum: ["1", "2", "3"] }; + const value1 = "1"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = "3"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 === undefined); + const value3 = "4"; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 !== undefined); + const value4 = undefined; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + }); + + it("pattern", async () => { + const validation: StringValidation = { pattern: "^[0-9a-z]+$" }; + const value1 = "1"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = "asb13"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 === undefined); + const value3 = "as--123"; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 !== undefined); + const value4 = undefined; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + }); + + it("startsWith", async () => { + const validation: StringValidation = { startsWith: "123" }; + const value1 = "123"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = "234"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 !== undefined); + const value3 = "1234"; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 === undefined); + const value4 = undefined; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + }); + + it("endsWith", async () => { + const validation: StringValidation = { endsWith: "123" }; + const value1 = "123"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = "234"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 !== undefined); + const value3 = "345sdf123"; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 === undefined); + const value4 = undefined; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + }); + + it("startsWith,endsWith", async () => { + const validation: StringValidation = { startsWith: "123", endsWith: "789" }; + const value1 = "123"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 !== undefined); + const value2 = "123asfsdwer7892345789"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 === undefined); + const value3 = "sadfws789"; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 !== undefined); + const value4 = undefined; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + }); + + it("includes", async () => { + const validation: StringValidation = { includes: "123" }; + const value1 = "123"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = "123asfsdwer7892345789"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 === undefined); + const value3 = "sadfws789"; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 !== undefined); + const value4 = undefined; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + }); + + it("includes,startsWith,endsWith", async () => { + const validation: StringValidation = { startsWith: "123", endsWith: "789", includes: "abc" }; + const value1 = "123"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 !== undefined); + const value2 = "123asfabcer7892345789"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 === undefined); + const value3 = "123sadfws789"; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 !== undefined); + const value4 = "abc789"; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + const value5 = undefined; + const res5 = await validate(validation, value5, inputs); + assert.isTrue(res5 !== undefined); + }); + + it("includes,startsWith,endsWith,maxLength", async () => { + const validation: StringValidation = { + startsWith: "123", + endsWith: "789", + includes: "abc", + maxLength: 10, + }; + const value1 = "123"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 !== undefined); + const value2 = "123asfabcer7892345789"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 !== undefined); + const value3 = "123sadfws789"; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 !== undefined); + const value4 = "123abch789"; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 === undefined); + }); + + it("excludesEnum", async () => { + const validation: StringValidation = { excludesEnum: ["1", "2", "3"] }; + const value1 = "1"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 !== undefined); + const value2 = "3"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 !== undefined); + const value3 = "4"; + const res3 = await validate(validation, value3, inputs); + assert.isUndefined(res3); + const value4 = undefined; + const res4 = await validate(validation, value4, inputs); + assert.isUndefined(res4); + }); + }); + + describe("StringArrayValidation", () => { + it("maxItems,minItems", async () => { + const validation: StringArrayValidation = { maxItems: 3, minItems: 1 }; + const value1 = ["1", "2", "3"]; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = ["1", "2", "3", "4"]; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 !== undefined); + const value3 = ["1", "2"]; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 === undefined); + const value4: string[] = []; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + const value5 = undefined; + const res5 = await validate(validation, value5, inputs); + assert.isTrue(res5 !== undefined); + }); + + it("uniqueItems", async () => { + const validation: StringArrayValidation = { uniqueItems: true }; + const value1 = ["1", "2", "3"]; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = ["1", "2", "1", "2"]; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 !== undefined); + const value3 = ["1", "2"]; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 === undefined); + const value4: string[] = []; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 === undefined); + const value5 = undefined; + const res5 = await validate(validation, value5, inputs); + assert.isTrue(res5 !== undefined); + }); + + it("equals", async () => { + const validation: StringArrayValidation = { equals: ["1", "2", "3"] }; + const value1 = ["1", "2", "3"]; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = ["1", "2", "1", "2"]; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 !== undefined); + const value3 = ["1", "2"]; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 !== undefined); + const value4: string[] = []; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + const value5 = undefined; + const res5 = await validate(validation, value5, inputs); + assert.isTrue(res5 !== undefined); + }); + + it("enum", async () => { + const validation: StringArrayValidation = { enum: ["1", "2", "3"] }; + const value1 = ["1", "2", "3"]; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = ["1", "2", "4", "2"]; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 !== undefined); + const value3 = ["1", "2"]; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 === undefined); + const value4: string[] = []; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 === undefined); + const value5 = undefined; + const res5 = await validate(validation, value5, inputs); + assert.isTrue(res5 !== undefined); + }); + + it("contains", async () => { + const validation: StringArrayValidation = { contains: "4" }; + const value1 = ["1", "2", "3"]; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 !== undefined); + const value2 = ["1", "2", "4", "2"]; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 === undefined); + const value3 = ["1", "2"]; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 !== undefined); + const value4: string[] = []; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + const value5 = undefined; + const res5 = await validate(validation, value5, inputs); + assert.isTrue(res5 !== undefined); + }); + + it("containsAll", async () => { + const validation: StringArrayValidation = { containsAll: ["1", "2"] }; + const value1 = ["1", "2", "3"]; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = ["1", "2", "4", "2"]; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 === undefined); + const value3 = ["1", "3"]; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 !== undefined); + const value4: string[] = []; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + const value5 = undefined; + const res5 = await validate(validation, value5, inputs); + assert.isTrue(res5 !== undefined); + }); + + it("containsAny", async () => { + const validation: StringArrayValidation = { containsAny: ["1", "2"] }; + const value1 = ["1", "2", "3"]; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 === undefined); + const value2 = ["4", "5", "6", "1"]; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 === undefined); + const value3 = ["5", "7"]; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 !== undefined); + const value4: string[] = []; + const res4 = await validate(validation, value4, inputs); + assert.isTrue(res4 !== undefined); + const value5 = undefined; + const res5 = await validate(validation, value5, inputs); + assert.isTrue(res5 !== undefined); + }); + }); + + it("FuncValidation", async () => { + const validation: FuncValidation = { + validFunc: function (input: string): string | undefined | Promise { + if ((input as string).length > 5) return "length > 5"; + return undefined; + }, + }; + const value1 = "123456"; + const res1 = await validate(validation, value1, inputs); + assert.isTrue(res1 !== undefined); + const value2 = "12345"; + const res2 = await validate(validation, value2, inputs); + assert.isTrue(res2 === undefined); + const value3 = ""; + const res3 = await validate(validation, value3, inputs); + assert.isTrue(res3 === undefined); + }); + it("FuncValidation 2", async () => { + const validation = (inputs: Inputs) => { + const input = inputs.input as string; + return input.length <= 5; + }; + const inputs: Inputs = { + platform: Platform.VSCode, + }; + inputs.input = "123456"; + const res1 = await validate(validation, "", inputs); + assert.isTrue(res1 !== undefined); + inputs.input = "12345"; + const res2 = await validate(validation, "", inputs); + assert.isTrue(res2 === undefined); + inputs.input = ""; + const res3 = await validate(validation, "", inputs); + assert.isTrue(res3 === undefined); + }); +}); diff --git a/packages/server/src/serverConnection.ts b/packages/server/src/serverConnection.ts index 113eea7b7a..d20b8c5d95 100644 --- a/packages/server/src/serverConnection.ts +++ b/packages/server/src/serverConnection.ts @@ -8,9 +8,9 @@ import { CreateProjectResult, Func, FxError, + IQTreeNode, Inputs, OpenAIPluginManifest, - QTreeNode, Result, Stage, Tools, @@ -92,13 +92,13 @@ export default class ServerConnection implements IServerConnection { this.connection.listen(); } - public async getQuestionsRequest( + public getQuestionsRequest( stage: Stage, inputs: Inputs, token: CancellationToken - ): Promise> { + ): Result { const corrId = inputs.correlationId ? inputs.correlationId : ""; - const res = await Correlator.runWithId( + const res = Correlator.runWithId( corrId, (stage, inputs) => this.core.getQuestions(stage, inputs), stage, diff --git a/packages/server/tests/serverConnection.test.ts b/packages/server/tests/serverConnection.test.ts index 09124bc7d5..94593d7df0 100644 --- a/packages/server/tests/serverConnection.test.ts +++ b/packages/server/tests/serverConnection.test.ts @@ -44,15 +44,16 @@ describe("serverConnections", () => { it("getQuestionsRequest", () => { const connection = new ServerConnection(msgConn); - const fake = sandbox.fake.returns(undefined); + const fake = sandbox.fake.returns(ok(undefined)); sandbox.replace(connection["core"], "getQuestions", fake); const stage = Stage.create; const inputs = { platform: Platform.VS }; const token = {}; const res = connection.getQuestionsRequest(stage, inputs as Inputs, token as CancellationToken); - res.then((data) => { - assert.equal(data, ok(undefined)); - }); + assert.isTrue(res.isOk()); + if (res.isOk()) { + assert.isUndefined(res.value); + } }); it("createProjectRequest", () => { diff --git a/packages/vscode-extension/test/mocks/mockCore.ts b/packages/vscode-extension/test/mocks/mockCore.ts index 18d72f9e65..05278d586e 100644 --- a/packages/vscode-extension/test/mocks/mockCore.ts +++ b/packages/vscode-extension/test/mocks/mockCore.ts @@ -4,9 +4,7 @@ import { Func, FxError, Inputs, - QTreeNode, Result, - Stage, ok, } from "@microsoft/teamsfx-api"; import { CoreCallbackFunc } from "@microsoft/teamsfx-core";