diff --git a/package.json b/package.json index d3dd0cfc4e..50bc115324 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "**/*.d.ts", "output/", "packages/cli/templates/**/files/", - "packages/igx-templates/", + "packages/igx-templates/**/files/", "packages/igniteui-mcp/" ], "reporter": [ diff --git a/packages/cli/lib/commands/doc.ts b/packages/cli/lib/commands/doc.ts index cef33ff543..98cc56a6c2 100644 --- a/packages/cli/lib/commands/doc.ts +++ b/packages/cli/lib/commands/doc.ts @@ -15,7 +15,7 @@ const doc: DocCommandType = { }, open: async (target) => { const open = await import("open"); - open.default.call(target); + await open.default(target); }, async handler(argv: ArgumentsCamelCase) { GoogleAnalytics.post({ diff --git a/packages/core/util/ProjectConfig.ts b/packages/core/util/ProjectConfig.ts index fd07d39f35..e07f27a2d9 100644 --- a/packages/core/util/ProjectConfig.ts +++ b/packages/core/util/ProjectConfig.ts @@ -91,7 +91,7 @@ export class ProjectConfig { const result = { message: undefined, valid: false, value: undefined }; if (!schema.properties.hasOwnProperty(property)) { - result.message = `Property "${property}" is not allowed in "${schema.title}" type!`; + result.message = `Property "${property}" is not allowed in the Ignite UI CLI configuration!`; return result; } diff --git a/packages/igx-templates/igx-ts/grid/grid-custom/index.ts b/packages/igx-templates/igx-ts/grid/grid-custom/index.ts index 778733801e..9836e847ba 100644 --- a/packages/igx-templates/igx-ts/grid/grid-custom/index.ts +++ b/packages/igx-templates/igx-ts/grid/grid-custom/index.ts @@ -81,9 +81,10 @@ class IgxCustomGridTemplate extends IgniteUIForAngularTemplate { const featuresUrls = []; for (const feature of this.userExtraConfiguration["columnFeatures"] as string[]) { switch (feature) { - case "Sorting": case "Filtering": gridFeatures.push(`[allowFiltering]="true"`); + // falls through + case "Sorting": case "Resizing": const text = `[${feature.toLowerCase().replace("ing", "able")}]="true"`; columnFeatures.push(text); diff --git a/packages/igx-templates/igx-ts/grid/multi-column-headers/index.ts b/packages/igx-templates/igx-ts/grid/multi-column-headers/index.ts index 07a82df2bf..38fa04d834 100644 --- a/packages/igx-templates/igx-ts/grid/multi-column-headers/index.ts +++ b/packages/igx-templates/igx-ts/grid/multi-column-headers/index.ts @@ -24,7 +24,7 @@ class IgxMultiColumnHeadersTemplate extends IgniteUIForAngularTemplate { public getExtraConfiguration(): ControlExtraConfiguration[] { return [{ - choices: ["Sorting", "Filtering", "Resizing", "Paging", "Column Pinning", "Moving"], + choices: ["Sorting", "Filtering", "Resizing", "Paging", "Moving"], default: "", key: "columnFeatures", message: "Choose features for the igx-grid", @@ -49,11 +49,13 @@ class IgxMultiColumnHeadersTemplate extends IgniteUIForAngularTemplate { case "Sorting": case "Filtering": case "Resizing": - case "Moving": const text = `[${feature.toLowerCase().replace("ing", "able")}]="true"`; columnFeatures.push(text); columnBoolFeatures.push(text); break; + case "Moving": + gridFeatures.push(`[moving]="true"`); + break; case "Row Selection": const gridFeatureText = `rowSelection="multiple"`; gridFeatures.push(gridFeatureText); diff --git a/packages/igx-templates/igx-ts/hierarchical-grid/hierarchical-grid-custom/index.ts b/packages/igx-templates/igx-ts/hierarchical-grid/hierarchical-grid-custom/index.ts index 1cd53d2e91..47eb64ae13 100644 --- a/packages/igx-templates/igx-ts/hierarchical-grid/hierarchical-grid-custom/index.ts +++ b/packages/igx-templates/igx-ts/hierarchical-grid/hierarchical-grid-custom/index.ts @@ -1,7 +1,20 @@ -import { ControlExtraConfigType, ControlExtraConfiguration } from "@igniteui/cli-core"; +import { ControlExtraConfigType, ControlExtraConfiguration, TemplateDependency } from "@igniteui/cli-core"; import { IgniteUIForAngularTemplate } from "../../../IgniteUIForAngularTemplate"; import { IGNITEUI_ANGULAR_PACKAGE } from "../../../constants"; +const BASE_DEPENDENCIES: TemplateDependency[] = [ + { + import: [ + "IgxHierarchicalGridComponent", + "IgxRowIslandComponent", + "IgxColumnComponent", + "IgxIconComponent" + ], + from: "<%=igxPackage%>", + standalone: true + } +]; + class IgxHierarchicalGridTemplate extends IgniteUIForAngularTemplate { private userExtraConfiguration = {}; private usePinning: boolean; @@ -16,18 +29,7 @@ class IgxHierarchicalGridTemplate extends IgniteUIForAngularTemplate { this.projectType = "igx-ts"; this.name = "Custom Hierarchical Grid"; this.description = "a customizable IgxHierarchicalGrid"; - this.dependencies = [ - { - import: [ - "IgxHierarchicalGridComponent", - "IgxRowIslandComponent", - "IgxColumnComponent", - "IgxIconComponent" - ], - from: "<%=igxPackage%>", - standalone: true - } - ]; + this.dependencies = [...BASE_DEPENDENCIES]; this.packages = [IGNITEUI_ANGULAR_PACKAGE]; this.hasExtraConfiguration = true; } @@ -48,6 +50,10 @@ class IgxHierarchicalGridTemplate extends IgniteUIForAngularTemplate { } public generateConfig(name: string, ...options: any[]): {[key: string]: any} { + // the template instance is shared, so reset state left over from previous calls + this.usePinning = false; + this.additionalElements = [""]; + this.dependencies = [...BASE_DEPENDENCIES]; const columnFeatures = []; const gridFeatures = []; const extraConfig = { @@ -139,57 +145,57 @@ class IgxHierarchicalGridTemplate extends IgniteUIForAngularTemplate { } featuresUrls.push(this.getFeatureUrl(feature, featureUrl)); - selectedFeatures = features.map((e, i) => { - anchorWrapper.href = featuresUrls[i]; - anchorWrapper.text = e; - return ` ${anchorWrapper.start}${anchorWrapper.href}${anchorWrapper.middle}` + - `${anchorWrapper.text}${anchorWrapper.end}`; - }).toString(); - if (selectedFeatures.length > 0) { - selectedFeatures = `

Active Features: ${selectedFeatures}

`; - } - if (toolbarActions.length) { - this.dependencies = [ - ...this.dependencies, - ...[ - { - standalone: true, - import: "IgxGridToolbarComponent", - from: "<%=igxPackage%>" - }, - { - standalone: true, - import: "IgxGridToolbarTitleComponent", - from: "<%=igxPackage%>" - }, - { - standalone: true, - import: "IgxGridToolbarActionsComponent", - from: "<%=igxPackage%>" - }, - { - standalone: true, - import: "IgxGridToolbarPinningComponent", - from: "<%=igxPackage%>" - }, - { - standalone: true, - import: "IgxGridToolbarHidingComponent", - from: "<%=igxPackage%>" - }, - ], - ]; + } + selectedFeatures = features.map((e, i) => { + anchorWrapper.href = featuresUrls[i]; + anchorWrapper.text = e; + return ` ${anchorWrapper.start}${anchorWrapper.href}${anchorWrapper.middle}` + + `${anchorWrapper.text}${anchorWrapper.end}`; + }).toString(); + if (selectedFeatures.length > 0) { + selectedFeatures = `

Active Features: ${selectedFeatures}

`; + } + if (toolbarActions.length) { + this.dependencies = [ + ...this.dependencies, + ...[ + { + standalone: true, + import: "IgxGridToolbarComponent", + from: "<%=igxPackage%>" + }, + { + standalone: true, + import: "IgxGridToolbarTitleComponent", + from: "<%=igxPackage%>" + }, + { + standalone: true, + import: "IgxGridToolbarActionsComponent", + from: "<%=igxPackage%>" + }, + { + standalone: true, + import: "IgxGridToolbarPinningComponent", + from: "<%=igxPackage%>" + }, + { + standalone: true, + import: "IgxGridToolbarHidingComponent", + from: "<%=igxPackage%>" + }, + ], + ]; - const parts = [ - " ", - " Singers", - " ", - ...toolbarActions, - " ", - " " - ]; - this.additionalElements.splice(1, 0, parts.join("\n")); - } + const parts = [ + " ", + " Singers", + " ", + ...toolbarActions, + " ", + " " + ]; + this.additionalElements.splice(1, 0, parts.join("\n")); } } diff --git a/packages/ng-schematics/src/utils/cli-config.spec.ts b/packages/ng-schematics/src/utils/cli-config.spec.ts new file mode 100644 index 0000000000..8dde055ed0 --- /dev/null +++ b/packages/ng-schematics/src/utils/cli-config.spec.ts @@ -0,0 +1,39 @@ +import { EmptyTree, SchematicContext, SchematicsException } from "@angular-devkit/schematics"; +import { UnitTestTree } from "@angular-devkit/schematics/testing"; +import { createCliConfig } from "./cli-config"; + +describe("createCliConfig", () => { + let tree: UnitTestTree; + let context: SchematicContext; + let rule: (tree: UnitTestTree, context: SchematicContext) => Promise; + + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + context = { + logger: jasmine.createSpyObj("logger", ["info", "warn"]) + } as unknown as SchematicContext; + rule = createCliConfig() as (tree: UnitTestTree, context: SchematicContext) => Promise; + }); + + it("should report a missing angular.json", async () => { + await expectAsync(rule(tree, context)) + .toBeRejectedWithError(SchematicsException, "angular.json was not found in the project's root"); + expect(tree.exists("ignite-ui-cli.json")).toBeFalse(); + }); + + it("should report a missing angular.json when the root has other files", async () => { + tree.create("package.json", "{}"); + + await expectAsync(rule(tree, context)) + .toBeRejectedWithError(SchematicsException, "angular.json was not found in the project's root"); + expect(tree.exists("ignite-ui-cli.json")).toBeFalse(); + }); + + it("should rethrow other workspace read errors", async () => { + tree.create("angular.json", "{ not json"); + + await expectAsync(rule(tree, context)) + .toBeRejectedWithError(Error, "Unknown format - version specifier not found."); + expect(tree.exists("ignite-ui-cli.json")).toBeFalse(); + }); +}); diff --git a/packages/ng-schematics/src/utils/cli-config.ts b/packages/ng-schematics/src/utils/cli-config.ts index 4df8e5ea09..0b2ad218b9 100644 --- a/packages/ng-schematics/src/utils/cli-config.ts +++ b/packages/ng-schematics/src/utils/cli-config.ts @@ -23,7 +23,9 @@ async function GetCliConfig(tree: Tree): Promise { try { ({ workspace } = await workspaces.readWorkspace("/", createWorkspaceHost(tree))); } catch (e) { - if (e.toString().includes("Could not find (undefined)")) { + const message = e.toString(); + if (message.includes("Unable to determine format for workspace path") || + message.includes("Unable to locate a workspace file")) { throw new SchematicsException("angular.json was not found in the project's root"); } throw new Error(e.message); diff --git a/spec/helpers/utils.ts b/spec/helpers/utils.ts index 301a9f2ed3..c7531705d9 100644 --- a/spec/helpers/utils.ts +++ b/spec/helpers/utils.ts @@ -47,3 +47,14 @@ export function filesDiff( export function resetSpy(spy: any) { (spy as jasmine.Spy).calls.reset(); } + +/** + * Loads a fresh copy of a module, bypassing the require cache. + * Use for modules that export a stateful singleton, like template instances. + * @param modulePath Absolute path to the module + */ +export function requireFresh(modulePath: string): T { + const resolved = require.resolve(modulePath); + delete require.cache[resolved]; + return require(resolved); +} diff --git a/spec/unit/IgniteUIForReactTemplate-spec.ts b/spec/unit/IgniteUIForReactTemplate-spec.ts new file mode 100644 index 0000000000..f4bbdc56be --- /dev/null +++ b/spec/unit/IgniteUIForReactTemplate-spec.ts @@ -0,0 +1,47 @@ +import { Util } from "@igniteui/cli-core"; +import { IgniteUIForReactTemplate } from "../../packages/cli/lib/templates/IgniteUIForReactTemplate"; + +describe("Unit - IgniteUIForReactTemplate", () => { + let template: IgniteUIForReactTemplate; + + beforeEach(() => { + spyOn(Util, "version").and.returnValue("1.0.0"); + template = new IgniteUIForReactTemplate("root/path"); + }); + + describe("generateConfig", () => { + it("generates the base variables from the component name", () => { + const config = template.generateConfig("my component", {}); + + expect(config).toEqual({ + path: "my-component", + name: "my component", + ClassName: "MyComponent", + filePrefix: "my-component", + cliVersion: "1.0.0" + }); + }); + + it("merges the extra configuration into the generated config", () => { + const config = template.generateConfig("my component", { + extraConfig: { selectionType: "multi", name: "overridden" } + }); + + expect(config["selectionType"]).toBe("multi"); + // base variables take precedence over extra configuration + expect(config["name"]).toBe("my component"); + expect(config["ClassName"]).toBe("MyComponent"); + }); + + it("adds widget and description variables when set", () => { + template["widget"] = "igr-grid"; + template.description = "Test description"; + + const config = template.generateConfig("my component", {}); + + expect(config["widget"]).toBe("igr-grid"); + expect(config["Control"]).toBe("IgrGrid"); + expect(config["description"]).toBe("Test description"); + }); + }); +}); diff --git a/spec/unit/ProjectConfig-spec.ts b/spec/unit/ProjectConfig-spec.ts index 5bea463156..360b5358e6 100644 --- a/spec/unit/ProjectConfig-spec.ts +++ b/spec/unit/ProjectConfig-spec.ts @@ -43,4 +43,12 @@ describe("Unit - ProjectConfig", () => { expect(ProjectConfig.hasLocalConfig()).toBeTruthy(); expect(fs.statSync).toHaveBeenCalledTimes(1); }); + + it("validateProperty rejects properties that are not in the schema", () => { + const result = ProjectConfig.validateProperty("notAConfigProperty", "value"); + expect(result.valid).toBeFalse(); + expect(result.value).toBeUndefined(); + expect(result.message) + .toBe(`Property "notAConfigProperty" is not allowed in the Ignite UI CLI configuration!`); + }); }); diff --git a/spec/unit/doc-spec.ts b/spec/unit/doc-spec.ts index e96f0e4c36..35821a8753 100644 --- a/spec/unit/doc-spec.ts +++ b/spec/unit/doc-spec.ts @@ -42,4 +42,18 @@ describe("Unit - Doc command", () => { expect(Util.error).toHaveBeenCalledWith("The search term 'Th1s i$ incorrect' is not valid." + "\n" + "Name should start with a letter and can also contain numbers, dashes and spaces.", "red"); }); + it("Should pass the target url to the 'open' package", async () => { + const openSpy = jasmine.createSpy("open").and.returnValue(Promise.resolve()); + const moduleLoader = require("module"); + const originalLoad = moduleLoader._load; + spyOn(moduleLoader, "_load").and.callFake(function (request: string, ...args: unknown[]) { + if (request === "open") { + return { __esModule: true, default: openSpy }; + } + return originalLoad.call(this, request, ...args); + }); + await doc.open("https://www.infragistics.com/search?q=igGrid"); + expect(openSpy).toHaveBeenCalledTimes(1); + expect(openSpy).toHaveBeenCalledWith("https://www.infragistics.com/search?q=igGrid"); + }); }); diff --git a/spec/unit/igx-templates/calendar-spec.ts b/spec/unit/igx-templates/calendar-spec.ts new file mode 100644 index 0000000000..2a36e5cf7b --- /dev/null +++ b/spec/unit/igx-templates/calendar-spec.ts @@ -0,0 +1,53 @@ +import { App, ControlExtraConfigType, Template } from "@igniteui/cli-core"; +import * as path from "path"; +import { requireFresh } from "../../helpers/utils"; + +const templatePath = path.join(__dirname, "../../../packages/igx-templates/igx-ts/calendar/default"); + +describe("Unit - igx-ts Calendar template", () => { + let template: Template; + + beforeAll(() => { + App.initialize(); + }); + + beforeEach(() => { + template = requireFresh