diff --git a/extensions/analyticsdx-vscode-templates/src/layout/completions.ts b/extensions/analyticsdx-vscode-templates/src/layout/completions.ts index cbdd6341..010e4108 100644 --- a/extensions/analyticsdx-vscode-templates/src/layout/completions.ts +++ b/extensions/analyticsdx-vscode-templates/src/layout/completions.ts @@ -5,7 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { matchJsonNodeAtPattern } from '@salesforce/analyticsdx-template-lint'; +import { matchJsonNodeAtPattern, matchJsonNodesAtPattern } from '@salesforce/analyticsdx-template-lint'; import { Location, parseTree } from 'jsonc-parser'; import * as vscode from 'vscode'; import { codeCompletionUsedTelemetryCommand } from '../telemetry'; @@ -16,6 +16,50 @@ import { isValidRelpath } from '../util/utils'; import { VariableRefCompletionItemProviderDelegate } from '../variables'; import { getLayoutItemVariableName, isInTilesEnumKey, matchesLayoutItem } from './utils'; +/** Get tags from the readiness file's templateRequirements. */ +export class LayoutValidationPageTagCompletionItemProviderDelegate implements JsonCompletionItemProviderDelegate { + constructor(private readonly templateEditing: TemplateDirEditing) {} + + public isSupportedDocument(document: vscode.TextDocument): boolean { + return ( + // make sure the template has a readinessDefinition file + isValidRelpath(this.templateEditing.readinessDefinitionPath) && + // and that we're in the layoutDefinition file of the template + this.templateEditing.isLayoutDefinitionFile(document.uri) + ); + } + + public isSupportedLocation(location: Location): boolean { + // Note: we should be checking that it's a validation page (and not a Configuration page, e.g.), but we don't + // get the parent node hierarchy in the Location passed in, and it's not that big a deal if the user gets a + // code-completion for this path in the layout.json file on a Configuration page since they'll already be getting + // errors about the wrong type + return !location.isAtPropertyKey && location.matches(['pages', '*', 'groups', '*', 'tags', '*']); + } + + public async getItems(range: vscode.Range | undefined, location: Location, document: vscode.TextDocument) { + const varUri = vscode.Uri.joinPath(this.templateEditing.dir, this.templateEditing.readinessDefinitionPath!); + const doc = await vscode.workspace.openTextDocument(varUri); + const tree = parseTree(doc.getText()); + const items: vscode.CompletionItem[] = []; + if (tree?.type === 'object') { + const tags = new Set( + matchJsonNodesAtPattern( + tree, + ['templateRequirements', '*', 'tags', '*'], + tagNode => typeof tagNode.value === 'string' + ).map(tagNode => tagNode.value as string) + ); + tags.forEach(tag => { + const item = newCompletionItem(tag, range, vscode.CompletionItemKind.EnumMember); + item.command = codeCompletionUsedTelemetryCommand(item.label, 'tag', location.path, document.uri); + items.push(item); + }); + } + return items; + } +} + /** Get variable names for the variable name in the pages in ui.json. */ export class LayoutVariableCompletionItemProviderDelegate extends VariableRefCompletionItemProviderDelegate { constructor(templateEditing: TemplateDirEditing) { diff --git a/extensions/analyticsdx-vscode-templates/src/templateEditing.ts b/extensions/analyticsdx-vscode-templates/src/templateEditing.ts index ee95cb21..652f25d1 100644 --- a/extensions/analyticsdx-vscode-templates/src/templateEditing.ts +++ b/extensions/analyticsdx-vscode-templates/src/templateEditing.ts @@ -43,6 +43,7 @@ import { } from './constants'; import { ERRORS } from './constants'; import { + LayoutValidationPageTagCompletionItemProviderDelegate, LayoutVariableCodeActionProvider, LayoutVariableCompletionItemProviderDelegate, LayoutVariableDefinitionProvider, @@ -367,6 +368,8 @@ export class TemplateDirEditing extends Disposable { new JsonCompletionItemProvider( // hookup code-completion for variables names in page in ui.json's new UiVariableCompletionItemProviderDelegate(this), + // hoopkup code-completion for validation page group tags in layout.json's + new LayoutValidationPageTagCompletionItemProviderDelegate(this), // hookup code-completion for variables names in page in layout.json's new LayoutVariableCompletionItemProviderDelegate(this), // hookup completions for tile names in variable items in layout.json's @@ -390,6 +393,14 @@ export class TemplateDirEditing extends Disposable { } ), + vscode.languages.registerCodeActionsProvider( + relatedFileSelector, + new FuzzyMatchCodeActionProvider(ERRORS.LAYOUT_VALIDATION_PAGE_UNKNOWN_GROUP_TAG), + { + providedCodeActionKinds: FuzzyMatchCodeActionProvider.providedCodeActionKinds + } + ), + // hookup quick fixes for variable names in ui.json's vscode.languages.registerCodeActionsProvider(relatedFileSelector, new UiVariableCodeActionProvider(this), { providedCodeActionKinds: UiVariableCodeActionProvider.providedCodeActionKinds diff --git a/extensions/analyticsdx-vscode-templates/test/vscode-integration/schemas/layoutSchema.test.ts b/extensions/analyticsdx-vscode-templates/test/vscode-integration/schemas/layoutSchema.test.ts index cd1333f0..9c208b95 100644 --- a/extensions/analyticsdx-vscode-templates/test/vscode-integration/schemas/layoutSchema.test.ts +++ b/extensions/analyticsdx-vscode-templates/test/vscode-integration/schemas/layoutSchema.test.ts @@ -56,6 +56,7 @@ describe('layout-schema.json hookup', () => { pages: [ { title: '', + type: 'Configuration', layout: { type: 'SingleColumn', center: { @@ -109,6 +110,7 @@ describe('layout-schema.json hookup', () => { pages: [ { title: '', + type: 'Configuration', layout: { type: 'SingleColumn', center: { diff --git a/extensions/analyticsdx-vscode-templates/test/vscode-integration/templateEditing/layout.test.ts b/extensions/analyticsdx-vscode-templates/test/vscode-integration/templateEditing/layout.test.ts index 025b2ae3..3b020e98 100644 --- a/extensions/analyticsdx-vscode-templates/test/vscode-integration/templateEditing/layout.test.ts +++ b/extensions/analyticsdx-vscode-templates/test/vscode-integration/templateEditing/layout.test.ts @@ -11,7 +11,7 @@ import { findNodeAtLocation, JSONPath, Node as JsonNode, parseTree } from 'jsonc import * as vscode from 'vscode'; import { ERRORS } from '../../../src/constants'; import { jsonpathFrom, scanLinesUntil, uriDirname, uriStat } from '../../../src/util/vscodeUtils'; -import { waitFor } from '../../testutils'; +import { jsoncParse, waitFor } from '../../testutils'; import { closeAllEditors, compareCompletionItems, @@ -133,7 +133,7 @@ describe('TemplateEditorManager configures layoutDefinition', () => { await verifyCompletionsContain(doc, position, 'New pages'); // and go to just after the [ in "pages" position = scan.end.translate({ characterDelta: 1 }); - await verifyCompletionsContain(doc, position, 'New SingleColumn page', 'New TwoColumn page'); + await verifyCompletionsContain(doc, position, 'New SingleColumn page', 'New TwoColumn page', 'New Validation page'); // go to just before the { in "layout" node = findNodeAtLocation(tree!, ['pages', 0, 'layout']); @@ -582,6 +582,7 @@ describe('TemplateEditorManager configures layoutDefinition', () => { pages: [ { title: 'Test Title', + type: 'Configuration', layout: { type: 'SingleColumn', center: { @@ -746,6 +747,7 @@ describe('TemplateEditorManager configures layoutDefinition', () => { pages: [ { title: 'Test Title', + type: 'Configuration', layout: { type: 'SingleColumn', center: { @@ -864,6 +866,7 @@ describe('TemplateEditorManager configures layoutDefinition', () => { pages: [ { title: 'Test Title', + type: 'Configuration', navigation: { label: 'Test Label' }, @@ -926,7 +929,94 @@ describe('TemplateEditorManager configures layoutDefinition', () => { // and the tile keys should have been updated const layoutTree = parseTree(layoutEditor.document.getText()); expect(layoutTree, 'layout.json').to.not.be.undefined; - console.log(findNodeAtLocation(layoutTree!, ['pages', 0, 'navigation'])); expect(findNodeAtLocation(layoutTree!, ['pages', 0, 'navigation']), 'navigation node').to.be.undefined; }); + + it('quick fixes on unrecongized validation page group tag', async () => { + const [t, [layoutEditor]] = await createTemplateWithRelatedFiles( + { + field: 'layoutDefinition', + path: 'layout.json', + initialJson: { + pages: [ + { + title: 'validation', + type: 'Validation', + groups: [{ text: '', tags: ['bar', 'fo'] }] + } + ] + } + }, + { + field: 'readinessDefinition', + path: 'readiness.json', + initialJson: { + templateRequirements: [ + { + expression: '{{Variables.foo}}', + tags: ['foo'] + } + ] + } + } + ); + tmpdir = t; + // should have 2 diagnostics about the tags + const diagnosticFilter = (d: vscode.Diagnostic) => d.code === ERRORS.LAYOUT_VALIDATION_PAGE_UNKNOWN_GROUP_TAG; + let diagnostics = ( + await waitForDiagnostics( + layoutEditor.document.uri, + ds => ds && ds.filter(diagnosticFilter).length === 2, + 'Initial 2 warnings on layout.json' + ) + ) + .filter(diagnosticFilter) + .sort(sortDiagnostics); + + expect(jsonpathFrom(diagnostics[0]), 'diagnostics[0] jsonpath').to.equal('pages[0].groups[0].tags[0]'); + expect(jsonpathFrom(diagnostics[1]), 'diagnostics[1] jsonpath').to.equal('pages[0].groups[0].tags[1]'); + + // the 1st tag warning should not have any quick fixes + let actions = await getCodeActions(layoutEditor.document.uri, diagnostics[0].range); + if (actions.length !== 0) { + expect.fail("Expected no code actions on 'bar', got [" + actions.map(a => a.title).join(', ') + ']'); + } + // the 2nd tag warning should have a quick fix + actions = await getCodeActions(layoutEditor.document.uri, diagnostics[1].range); + if (actions.length !== 1) { + expect.fail("Expected 1 code actions on 'fo', got [" + actions.map(a => a.title).join(', ') + ']'); + } + expect(actions[0].title, 'quick fix action title').to.equal("Switch to 'foo'"); + expect(actions[0].edit, 'quick fix action edit').to.not.be.undefined; + // run the action + if (!(await vscode.workspace.applyEdit(actions[0].edit!))) { + expect.fail(`Quick fix '${actions[0].title}' failed`); + } + + // that should make that diagnostic go away + diagnostics = ( + await waitForDiagnostics( + layoutEditor.document.uri, + ds => ds && ds.filter(diagnosticFilter).length === 1, + 'Only 1 warning on layout.json' + ) + ).filter(diagnosticFilter); + // and the tag should be fixed up + const layoutJson = jsoncParse(layoutEditor.document.getText()); + expect(layoutJson.pages[0].groups[0].tags[1], 'fixed tag').to.equal('foo'); + }); + + it('code completions on validation page group tags', async () => { + const uri = uriFromTestRoot(waveTemplatesUriPath, 'allRelpaths', 'layout.json'); + const [doc] = await openFile(uri, true); + await waitForDiagnostics(uri, d => d && d.length >= 1); + await waitForTemplateEditorManagerHas(await getTemplateEditorManager(), uriDirname(uri), true); + + const position = findPositionByJsonPath(doc, ['pages', 3, 'groups', 0, 'tags']); + expect(position, 'pages[3].groups[0].tags').to.not.be.undefined; + const completions = await verifyCompletionsContain(doc, position!.translate(0, 1), '"Tag1"', '"Tag2"'); + if (completions.length !== 2) { + expect.fail('Expected 2 completions, got: ' + completions.map(i => i.label).join(', ')); + } + }); }); diff --git a/extensions/analyticsdx-vscode-templates/test/vscode-integration/templateLinter/layout.test.ts b/extensions/analyticsdx-vscode-templates/test/vscode-integration/templateLinter/layout.test.ts index b5d3044d..f0b7a4f3 100644 --- a/extensions/analyticsdx-vscode-templates/test/vscode-integration/templateLinter/layout.test.ts +++ b/extensions/analyticsdx-vscode-templates/test/vscode-integration/templateLinter/layout.test.ts @@ -51,6 +51,7 @@ describe('TemplateLinterManager lints layout.json', () => { pages: [ { title: 'Page1', + type: 'Configuration', layout: { type: 'TwoColumn', left: { @@ -63,6 +64,7 @@ describe('TemplateLinterManager lints layout.json', () => { }, { title: 'Page2', + type: 'Configuration', layout: { type: 'SingleColumn', center: { @@ -204,6 +206,7 @@ describe('TemplateLinterManager lints layout.json', () => { pages: [ { title: 'Page1', + type: 'Configuration', layout: { type: 'SingleColumn', right: { diff --git a/packages/analyticsdx-template-lint/src/constants.ts b/packages/analyticsdx-template-lint/src/constants.ts index d0ce1aa0..f5a38697 100644 --- a/packages/analyticsdx-template-lint/src/constants.ts +++ b/packages/analyticsdx-template-lint/src/constants.ts @@ -185,6 +185,10 @@ export const ERRORS = Object.freeze({ LAYOUT_INVALID_TILE_NAME: 'lay-5', /** Unsupported variable type in layout page */ LAYOUT_PAGE_UNNECESSARY_NAVIGATION_OBJECT: 'lay-6', + /** Validation page group tag doesn't match a readiness templateRequirement tag. */ + LAYOUT_VALIDATION_PAGE_UNKNOWN_GROUP_TAG: 'lay-7', + /** Multiple incldueUnmatched: true groups in a validation page. */ + LAYOUT_VALIDATION_PAGE_MULTIPLE_INCLUDE_UNMATCHED: 'lay-8', /** ApexCallback readiness definition but template has no apexCallback */ READINESS_NO_APEX_CALLBACK: 'read-1', diff --git a/packages/analyticsdx-template-lint/src/linter.ts b/packages/analyticsdx-template-lint/src/linter.ts index fca3780f..f110dfb9 100644 --- a/packages/analyticsdx-template-lint/src/linter.ts +++ b/packages/analyticsdx-template-lint/src/linter.ts @@ -8,6 +8,7 @@ import { findNodeAtLocation, JSONPath, Node as JsonNode, parseTree } from 'jsonc-parser'; import { ERRORS, LINTER_MAX_EXTERNAL_FILE_SIZE, TEMPLATE_INFO } from './constants'; import { + caching, fuzzySearcher, isValidRelpath, isValidVariableName, @@ -1132,10 +1133,12 @@ export abstract class TemplateLinter< private async lintLayout(templateInfo: JsonNode) { const { doc, json: layout } = await this.loadTemplateRelPathJson(templateInfo, ['layoutDefinition']); if (doc && layout) { - await Promise.all([ + const promises = Promise.all([ this.lintLayoutCheckVariables(templateInfo, doc, layout), - this.lintLayoutCheckNavigationObjects(doc, layout) + this.lintLayoutValidationPages(templateInfo, doc, layout) ]); + this.lintLayoutCheckNavigationObjects(doc, layout); + return promises; } } @@ -1233,7 +1236,78 @@ export abstract class TemplateLinter< } } - private async lintLayoutCheckNavigationObjects(doc: Document, layoutJson: JsonNode): Promise { + private async lintLayoutValidationPages(templateInfo: JsonNode, doc: Document, layoutJson: JsonNode) { + // function to read all the templateRequirement tags from the readiness file + const readinessTags = caching(async () => { + const { json: readinessJson } = await this.loadTemplateRelPathJson(templateInfo, ['readinessDefinition']); + return new Set( + matchJsonNodesAtPattern( + readinessJson, + ['templateRequirements', '*', 'tags', '*'], + tag => typeof tag.value === 'string' + ).map(tag => tag.value as string) + ); + }); + const fuzzyMatcher = caching((tags: Set) => fuzzySearcher(tags)); + + // loop through each Validation page + const pages = matchJsonNodesAtPattern( + layoutJson, + ['pages', '*'], + page => findJsonPrimitiveAttributeValue(page, 'type')[0] === 'Validation' + ); + for (const page of pages) { + const includeUnmatchedNodes = [] as JsonNode[]; + // go through this page's groups + for (const group of matchJsonNodesAtPattern(page, ['groups', '*'])) { + // make sure any tags have corresponding entries in the readiness file + const tagNodes = matchJsonNodesAtPattern(group, ['tags', '*'], tagNode => typeof tagNode.value === 'string'); + for (const tagNode of tagNodes) { + const tags = await readinessTags(); + const tag = tagNode.value as string; + if (!tags.has(tag)) { + const args: Record = { name: tag }; + let mesg = `Tag '${tag}' not found in readiness definition`; + const [match] = fuzzyMatcher(tags)(tag); + if (match) { + mesg += `, did you mean '${match}'?`; + args.match = match; + } + this.addDiagnostic(doc, mesg, ERRORS.LAYOUT_VALIDATION_PAGE_UNKNOWN_GROUP_TAG, tagNode, { args }); + } + } + + // keep track of any true includeUnmatched's + const [includeUnmatched, includeUnmatchedNode] = findJsonPrimitiveAttributeValue(group, 'includeUnmatched'); + if (includeUnmatched === true) { + includeUnmatchedNodes.push(includeUnmatchedNode!); + } + } + + // warn if there's more than 1 true includeUnmatched in the page + if (includeUnmatchedNodes.length > 1) { + includeUnmatchedNodes.forEach(includeUnmatchedNode => + this.addDiagnostic( + doc, + 'Multiple groups found with includeUnmatched true', + ERRORS.LAYOUT_VALIDATION_PAGE_MULTIPLE_INCLUDE_UNMATCHED, + includeUnmatchedNode, + { + relatedInformation: includeUnmatchedNodes + .filter(other => other !== includeUnmatchedNode) + .map(other => ({ + doc, + mesg: 'Other includeUnmatched', + node: other + })) + } + ) + ); + } + } + } + + private lintLayoutCheckNavigationObjects(doc: Document, layoutJson: JsonNode) { // Check to see if there is a navigationPanel object const navigationPanelNode = matchJsonNodeAtPattern(layoutJson, ['navigationPanel']); if (!navigationPanelNode) { diff --git a/packages/analyticsdx-template-lint/src/schemas/layout-schema.json b/packages/analyticsdx-template-lint/src/schemas/layout-schema.json index 42d81800..df788f1e 100644 --- a/packages/analyticsdx-template-lint/src/schemas/layout-schema.json +++ b/packages/analyticsdx-template-lint/src/schemas/layout-schema.json @@ -14,9 +14,16 @@ "description": "", "items": { "type": "object", - "required": ["title", "layout"], + "required": ["type", "title"], "additionalProperties": false, "properties": { + "type": { + "type": "string", + "description": "The type of page.", + "enum": ["Configuration", "Validation"], + "enumDescriptions": ["A configuration page.", "An org validation page."], + "default": "Configuration" + }, "title": { "type": "string", "description": "Title displayed at top of the page." @@ -46,135 +53,105 @@ "$ref": "#/definitions/backgroundImage", "description": "Image to display in the background of this page. It should be a horizontal image and it will be fixed at the bottom of the page." }, - "layout": { - "type": "object", - "description": "The layout specification for the page.", - "additionalProperties": false, - "required": ["type"], + "navigation": { + "$ref": "#/definitions/navigation", + "description": "Configure the node in the navigation panel for this page." + }, + "guidancePanel": { + "$ref": "#/definitions/guidancePanel", + "description": "The guidance panel specification for this page." + }, + "layout": { "doNotSuggest": true }, + "header": { "doNotSuggest": true }, + "groups": { "doNotSuggest": true } + }, + "oneOf": [ + { + "required": ["type", "layout"], "properties": { "type": { - "type": "string", - "description": "Layout type", - "enum": ["SingleColumn", "TwoColumn"], - "enumDescriptions": [ - "A page layout with a single panel of items.", - "A page layout with left and right panels of items." - ] + "const": "Configuration" }, - "header": { - "type": ["null", "object"], - "additionalProperties": false, - "description": "Header text to display at the top of the page, under the title.", - "properties": { - "text": { - "type": ["null", "string"], - "description": "Header text. This can contain {{...}} expressions.", - "defaultSnippets": [{ "label": "\"\"", "body": "${0}" }] - }, - "description": { - "type": ["null", "string"], - "description": "Header description text, displayed under the header text. This can contain {{...}} expressions.", - "defaultSnippets": [{ "label": "\"\"", "body": "${0}" }] - } - }, - "defaultSnippets": [ - { - "label": "New header", - "body": { - "text": "${1:Header text}", - "description": "${2}" - } - } - ] + "groups": { + "doNotSuggest": true, + "errorMessage": "Property groups is not allowed.", + "not": true }, - "center": { - "doNotSuggest": true - }, - "right": { - "doNotSuggest": true + "header": { + "doNotSuggest": true, + "errorMessage": "Property header is not allowed.", + "not": true }, - "left": { - "doNotSuggest": true + "layout": { + "$ref": "#/definitions/layout", + "description": "The layout specification for the page.", + "doNotSuggest": false } - }, - "anyOf": [ - { - "properties": { - "type": { - "const": "SingleColumn" - }, - "center": { - "$ref": "#/definitions/panel", - "description": "The panel of items for this page.", - "doNotSuggest": false - } - }, - "required": ["type", "center"] + } + }, + { + "required": ["type"], + "properties": { + "type": { + "const": "Validation" }, - { - "properties": { - "type": { - "const": "TwoColumn" - }, - "left": { - "$ref": "#/definitions/panel", - "description": "The left side panel of items for this page.", - "doNotSuggest": false - }, - "right": { - "$ref": "#/definitions/panel", - "description": "The right side panel of items for this page.", - "doNotSuggest": false - } - }, - "required": ["type", "left", "right"] + "layout": { + "doNotSuggest": true, + "errorMessage": "Property layout is not allowed.", + "not": true }, - { - "properties": { - "type": { - "not": { "enum": ["SingleColumn", "TwoColumn"] } - } - } - } - ], - "defaultSnippets": [ - { - "label": "New SingleColumn layout", - "body": { - "type": "SingleColumn", - "center": { - "items": [] - } - } + "header": { + "$ref": "#/definitions/header", + "doNotSuggest": false }, - { - "label": "New TwoColumn layout", - "body": { - "type": "TwoColumn", - "left": { - "items": [] + "groups": { + "description": "Specify how to group and display org validation results.", + "type": ["array", "null"], + "doNotSuggest": false, + "items": { + "type": "object", + "required": ["text"], + "additionalProperties": false, + "properties": { + "text": { + "type": "string", + "description": "Label to show for this group." + }, + "tags": { + "type": ["array", "null"], + "description": "Optional tags for this group, should correspond to templateRequirement tag(s) in the readiness definition file.", + "items": { + "type": "string" + }, + "defaultSnippets": [{ "label": "[]", "body": ["${0}"] }] + }, + "includeUnmatched": { + "type": ["boolean", "null"], + "description": "Set to true to include any validation results that don't match other groups, should be specified on only one group." + } }, - "right": { - "items": [] - } + "defaultSnippets": [ + { + "label": "New group", + "body": { + "text": "${0}" + } + } + ] } } - ] - }, - "navigation": { - "$ref": "#/definitions/navigation", - "description": "Configure the node in the navigation panel for this page." + } }, - "guidancePanel": { - "$ref": "#/definitions/guidancePanel", - "description": "The guidance panel specification for this page." + { + "properties": { "type": { "not": { "enum": ["Configuration", "Validation"] } } } } - }, + ], "defaultSnippets": [ { "label": "New SingleColumn page", "body": { "title": "${1:Page title}", + "type": "Configuration", "layout": { "type": "SingleColumn", "center": { @@ -187,6 +164,7 @@ "label": "New TwoColumn page", "body": { "title": "${1:Page title}", + "type": "Configuration", "layout": { "type": "TwoColumn", "left": { @@ -197,6 +175,14 @@ } } } + }, + { + "label": "New Validation page", + "body": { + "title": "${1:Page title}", + "type": "Validation", + "groups": [] + } } ] }, @@ -206,6 +192,7 @@ "body": [ { "title": "${1:Page title}", + "type": "Configuration", "layout": { "type": "SingleColumn", "center": { @@ -747,6 +734,124 @@ } ] }, + "header": { + "type": ["null", "object"], + "additionalProperties": false, + "description": "Header text to display at the top of the page, under the title.", + "properties": { + "text": { + "type": ["null", "string"], + "description": "Header text. This can contain {{...}} expressions.", + "defaultSnippets": [{ "label": "\"\"", "body": "${0}" }] + }, + "description": { + "type": ["null", "string"], + "description": "Header description text, displayed under the header text. This can contain {{...}} expressions.", + "defaultSnippets": [{ "label": "\"\"", "body": "${0}" }] + } + }, + "defaultSnippets": [ + { + "label": "New header", + "body": { + "text": "${1:Header text}", + "description": "${2}" + } + } + ] + }, + "layout": { + "type": "object", + "description": "The layout specification for the page.", + "additionalProperties": false, + "required": ["type"], + "properties": { + "type": { + "type": "string", + "description": "Layout type", + "enum": ["SingleColumn", "TwoColumn"], + "enumDescriptions": [ + "A page layout with a single panel of items.", + "A page layout with left and right panels of items." + ] + }, + "header": { + "$ref": "#/definitions/header" + }, + "center": { + "doNotSuggest": true + }, + "right": { + "doNotSuggest": true + }, + "left": { + "doNotSuggest": true + } + }, + "anyOf": [ + { + "properties": { + "type": { + "const": "SingleColumn" + }, + "center": { + "$ref": "#/definitions/panel", + "description": "The panel of items for this page.", + "doNotSuggest": false + } + }, + "required": ["type", "center"] + }, + { + "properties": { + "type": { + "const": "TwoColumn" + }, + "left": { + "$ref": "#/definitions/panel", + "description": "The left side panel of items for this page.", + "doNotSuggest": false + }, + "right": { + "$ref": "#/definitions/panel", + "description": "The right side panel of items for this page.", + "doNotSuggest": false + } + }, + "required": ["type", "left", "right"] + }, + { + "properties": { + "type": { + "not": { "enum": ["SingleColumn", "TwoColumn"] } + } + } + } + ], + "defaultSnippets": [ + { + "label": "New SingleColumn layout", + "body": { + "type": "SingleColumn", + "center": { + "items": [] + } + } + }, + { + "label": "New TwoColumn layout", + "body": { + "type": "TwoColumn", + "left": { + "items": [] + }, + "right": { + "items": [] + } + } + } + ] + }, "navigation": { "type": "object", "additionalProperties": false, diff --git a/packages/analyticsdx-template-lint/src/schemas/readiness-schema.json b/packages/analyticsdx-template-lint/src/schemas/readiness-schema.json index 96d8fdcb..5bf15d3d 100644 --- a/packages/analyticsdx-template-lint/src/schemas/readiness-schema.json +++ b/packages/analyticsdx-template-lint/src/schemas/readiness-schema.json @@ -10,7 +10,7 @@ "properties": { "values": { "description": "Default values for variables when computing readiness. Any values passed into the readiness check call will override these.", - "type": ["null", "object"], + "type": ["object", "null"], "additionalProperties": false, "patternProperties": { "^[a-zA-Z_][a-zA-Z0-9_]*$": {} @@ -19,7 +19,7 @@ }, "templateRequirements": { "description": "Expressions used to determine if the current org meets the requirements for this template. The template is considered ready if all these requirements are met.", - "type": ["null", "array"], + "type": ["array", "null"], "minItems": 0, "items": { "type": "object", @@ -34,7 +34,8 @@ "${Readiness.sobjectCount > Variables.minimumCount}", "${Readiness.datasetRowCount >= Constants.minNumRows}", "${Readiness.apexResult.name == 'requiredName'}" - ] + ], + "defaultSnippets": [{ "label": "\"\"", "body": "${0}" }] }, "type": { "description": "Optional definition type of the requirement. This can be used to badge or group readiness results.", @@ -66,23 +67,25 @@ ] }, "successMessage": { - "type": ["null", "string"], + "type": ["string", "null"], "description": "Optional message to return when the expression evaluates to true. This can access variable values as Variables.[name], constants from templateToApp rules files as Constants.[name], and computed readiness definition values as Readiness.[name].", "examples": [ "Succesfully found ${Readiness.sobjectCount} ${Variables.sobject.objectName} objects.", "Dataset ${App.Datasets.Opptys.Name} has at least ${Constants.rowCount} rows" - ] + ], + "defaultSnippets": [{ "label": "\"\"", "body": "${0}" }] }, "failMessage": { - "type": ["null", "string"], + "type": ["string", "null"], "description": "Optional message to return when the expression evaluates to false. This can access variable values as Variables.[name], constants from templateToApp rules files as Constants.[name], and computed readiness definition values as Readiness.[name].", "examples": [ "Too many ${Variables.sobject.objectName} objects, found ${Readiness.sobjectCount}", "${App.Datasets.Opptys.Name} only has ${Readiness.rowCount} rows, expected ${Constants.minRowCount}" - ] + ], + "defaultSnippets": [{ "label": "\"\"", "body": "${0}" }] }, "image": { - "type": ["null", "object"], + "type": ["object", "null"], "description": "Optional image that can be used when displaying the readiness results.", "additionalProperties": false, "required": ["name"], @@ -108,12 +111,12 @@ }, "tags": { "description": "Optional tags to associate with this requirement, which can be used to badge or group readiness results.", - "type": ["null", "array"], + "type": ["array", "null"], "minItems": 0, "items": { "type": "string" }, - "defaultSnippets": [{ "label": "[]", "body": [] }] + "defaultSnippets": [{ "label": "[]", "body": ["${0}"] }] } }, "defaultSnippets": [ diff --git a/packages/analyticsdx-template-lint/src/utils.ts b/packages/analyticsdx-template-lint/src/utils.ts index c928cb7e..3c6d777d 100644 --- a/packages/analyticsdx-template-lint/src/utils.ts +++ b/packages/analyticsdx-template-lint/src/utils.ts @@ -241,3 +241,26 @@ export function fuzzySearcher( return fuzzer.search(pattern, searchOpts).map(result => result.item); }; } + +/** Create a function that will cache the result of the underlying function on the first call, and + * return that result from there out. + */ +export function caching(fn: (this: T, ...arg: A) => R): (this: T, ...arg: A) => R { + let result: R; + let resultError: unknown | undefined; + let _fn: ((this: T, ...args: A) => R) | undefined = fn; + return function (this: T, ...args: A) { + if (_fn !== undefined) { + try { + result = _fn.apply(this, args); + } catch (error) { + resultError = error; + } + _fn = undefined; + } + if (resultError) { + throw resultError; + } + return result; + }; +} diff --git a/packages/analyticsdx-template-lint/test/testutils.ts b/packages/analyticsdx-template-lint/test/testutils.ts index f811aa76..c2c1177d 100644 --- a/packages/analyticsdx-template-lint/test/testutils.ts +++ b/packages/analyticsdx-template-lint/test/testutils.ts @@ -95,7 +95,8 @@ function newVscodeAjv(options?: AjvOptions): Ajv { 'deprecationMessage', 'doNotSuggest', 'enumDescriptions', - 'patternErrorMessage' + 'patternErrorMessage', + 'errorMessage' ]); return ajv; } diff --git a/packages/analyticsdx-template-lint/test/unit/linter/layout.test.ts b/packages/analyticsdx-template-lint/test/unit/linter/layout.test.ts index 12d48f95..1092d997 100644 --- a/packages/analyticsdx-template-lint/test/unit/linter/layout.test.ts +++ b/packages/analyticsdx-template-lint/test/unit/linter/layout.test.ts @@ -37,6 +37,7 @@ describe('TemplateLinter layout.json', () => { pages: [ { title: '', + type: 'Configuration', layout: { type: 'SingleColumn', center: { @@ -56,6 +57,7 @@ describe('TemplateLinter layout.json', () => { }, { title: '', + type: 'Configuration', layout: { type: 'TwoColumn', left: { @@ -114,6 +116,7 @@ describe('TemplateLinter layout.json', () => { pages: [ { title: '', + type: 'Configuration', layout: { type: 'SingleColumn', center: { @@ -169,6 +172,7 @@ describe('TemplateLinter layout.json', () => { pages: [ { title: '', + type: 'Configuration', layout: { type: 'SingleColumn', center: { @@ -273,6 +277,7 @@ describe('TemplateLinter layout.json', () => { pages: [ { title: '', + type: 'Configuration', layout: { type: 'SingleColumn', center: { @@ -291,7 +296,7 @@ describe('TemplateLinter layout.json', () => { await linter.lint(); const diagnostics = getDiagnosticsForPath(linter.diagnostics, layoutPath) || []; if (diagnostics.length !== 2) { - expect.fail('Expected 2 unsupported variable errors, got' + stringifyDiagnostics(diagnostics)); + expect.fail('Expected 2 unsupported variable errors, got ' + stringifyDiagnostics(diagnostics)); } let diagnostic = diagnostics.find(d => d.jsonpath === 'pages[0].navigation'); @@ -304,4 +309,129 @@ describe('TemplateLinter layout.json', () => { .undefined; expect(diagnostic!.code).to.equal(ERRORS.LAYOUT_PAGE_UNNECESSARY_NAVIGATION_OBJECT); }); + + it('validates validation page group tags', async () => { + const dir = 'validationTags'; + const layoutPath = path.join(dir, 'layout.json'); + linter = new TestLinter( + dir, + { + templateType: 'data', + readinessDefinition: 'readiness.json', + layoutDefinition: 'layout.json' + }, + new StringDocument(path.join(dir, 'readiness.json'), { + templateRequirements: [ + { expression: '{{Variables.foo}}}', tags: ['foo'] }, + { expression: '{{Variables.bar}}}', tags: ['foo', 'bar'] } + ] + }), + new StringDocument(layoutPath, { + pages: [ + { + title: 'valid tags', + type: 'Validation', + groups: [ + { text: '' }, + { text: '', tags: [] }, + { text: '', tags: ['foo'] }, + { text: '', tags: ['foo', 'bar'] } + ] + }, + { + title: 'invalid tags', + type: 'Validation', + groups: [ + { text: '', tags: ['', 'fo'] }, + { text: '', tags: ['baz', 'shouldnotmatchanything'] } + ] + } + ] + }) + ); + + await linter.lint(); + const diagnostics = getDiagnosticsForPath(linter.diagnostics, layoutPath) || []; + if (diagnostics.length !== 4) { + expect.fail('Expected 4 invalid tag errors, got ' + stringifyDiagnostics(diagnostics)); + } + + let diagnostic = diagnostics.find(d => d.jsonpath === 'pages[1].groups[0].tags[0]'); + expect(diagnostic, 'group[0].tag[0]').to.not.be.undefined; + expect(diagnostic!.code, 'group[0].tag[0] code').to.equal(ERRORS.LAYOUT_VALIDATION_PAGE_UNKNOWN_GROUP_TAG); + expect(diagnostic!.args, 'group[0].tag[0] args').to.not.be.undefined; + expect(diagnostic!.args!.name, 'group[0].tag[0] args name').to.equal(''); + expect(diagnostic!.args!.match, 'group[0].tag[0] args match').to.be.undefined; + + diagnostic = diagnostics.find(d => d.jsonpath === 'pages[1].groups[0].tags[1]'); + expect(diagnostic, 'group[0].tag[1]').to.not.be.undefined; + expect(diagnostic!.code, 'group[0].tag[1] code').to.equal(ERRORS.LAYOUT_VALIDATION_PAGE_UNKNOWN_GROUP_TAG); + expect(diagnostic!.args, 'group[0].tag[1] args').to.not.be.undefined; + expect(diagnostic!.args!.name, 'group[0].tag[1] args name').to.equal('fo'); + expect(diagnostic!.args!.match, 'group[0].tag[1] args match').to.equal('foo'); + + diagnostic = diagnostics.find(d => d.jsonpath === 'pages[1].groups[1].tags[0]'); + expect(diagnostic, 'group[1].tag[0]').to.not.be.undefined; + expect(diagnostic!.code, 'group[1].tag[1] code').to.equal(ERRORS.LAYOUT_VALIDATION_PAGE_UNKNOWN_GROUP_TAG); + expect(diagnostic!.args, 'group[1].tag[0] args').to.not.be.undefined; + expect(diagnostic!.args!.name, 'group[1].tag[0] args name').to.equal('baz'); + expect(diagnostic!.args!.match, 'group[1].tag[0] args match').to.equal('bar'); + + diagnostic = diagnostics.find(d => d.jsonpath === 'pages[1].groups[1].tags[1]'); + expect(diagnostic, 'group[1].tag[1]').to.not.be.undefined; + expect(diagnostic!.code, 'group[1].tag[1] code').to.equal(ERRORS.LAYOUT_VALIDATION_PAGE_UNKNOWN_GROUP_TAG); + expect(diagnostic!.args, 'group[1].tag[1] args').to.not.be.undefined; + expect(diagnostic!.args!.name, 'group[1].tag[1] args name').to.equal('shouldnotmatchanything'); + expect(diagnostic!.args!.match, 'group[1].tag[1] args match').to.be.undefined; + }); + + it('validates validation page group includeUnmatched', async () => { + const dir = 'includeUnmatched'; + const layoutPath = path.join(dir, 'layout.json'); + linter = new TestLinter( + dir, + { + templateType: 'data', + layoutDefinition: 'layout.json' + }, + new StringDocument(layoutPath, { + pages: [ + { + title: 'valid includeUnmatcheds', + type: 'Validation', + groups: [ + { text: '', includeUnmatched: true }, + { text: '', includeUnmatched: false }, + { text: '', includeUnmatched: null }, + { text: '' } + ] + }, + { + title: 'mulitple invaludeUnmatcheds', + type: 'Validation', + groups: [ + { text: '', includeUnmatched: true }, + { text: '', includeUnmatched: false }, + { text: '', includeUnmatched: true }, + { text: '' } + ] + } + ] + }) + ); + + await linter.lint(); + const diagnostics = getDiagnosticsForPath(linter.diagnostics, layoutPath) || []; + if (diagnostics.length !== 2) { + expect.fail('Expected 2 multiple includeUnmatched errors, got ' + stringifyDiagnostics(diagnostics)); + } + + let diagnostic = diagnostics.find(d => d.jsonpath === 'pages[1].groups[0].includeUnmatched'); + expect(diagnostic, 'group[0]').to.not.be.undefined; + expect(diagnostic!.code, 'group[0] code').to.equal(ERRORS.LAYOUT_VALIDATION_PAGE_MULTIPLE_INCLUDE_UNMATCHED); + + diagnostic = diagnostics.find(d => d.jsonpath === 'pages[1].groups[2].includeUnmatched'); + expect(diagnostic, 'group[2]').to.not.be.undefined; + expect(diagnostic!.code, 'group[2] code').to.equal(ERRORS.LAYOUT_VALIDATION_PAGE_MULTIPLE_INCLUDE_UNMATCHED); + }); }); diff --git a/packages/analyticsdx-template-lint/test/unit/schemas/invalidLayoutJsonTests.ts b/packages/analyticsdx-template-lint/test/unit/schemas/invalidLayoutJsonTests.ts index 413396aa..209bb4a9 100644 --- a/packages/analyticsdx-template-lint/test/unit/schemas/invalidLayoutJsonTests.ts +++ b/packages/analyticsdx-template-lint/test/unit/schemas/invalidLayoutJsonTests.ts @@ -28,6 +28,7 @@ describe('layout-schema.json finds errors in', () => { 'pages[0].layout.center.items[2].items[2].name', 'pages[0].layout.center.items[3].variant', 'pages[1].layout.type', + 'pages[2].type', 'displayMessages[0].location', 'pages[0].guidancePanel.items[0].type', 'pages[0].guidancePanel.items[0].variant', @@ -44,6 +45,8 @@ describe('layout-schema.json finds errors in', () => { false, 'error', 'pages[0].error', + 'pages[0].groups', + 'pages[0].header', 'pages[0].backgroundImage.error', 'pages[0].layout.error', 'pages[0].layout.header.error', @@ -63,6 +66,11 @@ describe('layout-schema.json finds errors in', () => { 'pages[0].guidancePanel.items[1].image.error', 'pages[0].guidancePanel.items[1].error', 'pages[0].guidancePanel.items[2].error', + 'pages[1].error', + 'pages[1].layout', + 'pages[1].header.error', + 'pages[1].backgroundImage.error', + 'pages[1].groups[0].error', 'appDetails.error' ); }); diff --git a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-enums.json b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-enums.json index a8b3384b..0d95b5e3 100644 --- a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-enums.json +++ b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-enums.json @@ -2,6 +2,7 @@ "pages": [ { "title": "Page title", + "type": "Configuration", "layout": { "type": "SingleColumn", "center": { @@ -64,12 +65,17 @@ }, { "title": "Page title", + "type": "Configuration", "layout": { "type": "badvalue", "center": { "items": [] } } + }, + { + "title": "Invalid type", + "type": "invalid" } ], "displayMessages": [ diff --git a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-fields.json b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-fields.json index 3fdacc9a..2f0eeed3 100644 --- a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-fields.json +++ b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-fields.json @@ -2,7 +2,10 @@ "error": "This should trigger a warning", "pages": [ { + "type": "Configuration", "error": "This should trigger a warning", + "groups": [{ "text": "groups should be invalid in Configuration page" }], + "header": { "text": "top-level header should be invalid in Configuration page" }, "backgroundImage": { "error": "This should trigger a warning", "name": "SentimentAnalysisBackground" @@ -97,6 +100,27 @@ } ] } + }, + { + "title": "Validation page", + "type": "Validation", + "error": "This should trigger a warning", + // layout shouldn't be allowed in a validation page + "layout": null, + "header": { + "error": "This should trigger a warning", + "text": "top-level header should be invalid in Configuration page" + }, + "backgroundImage": { + "error": "This should trigger a warning", + "name": "SentimentAnalysisBackground" + }, + "groups": [ + { + "error": "This should trigger a warning", + "text": "" + } + ] } ], "displayMessages": [ diff --git a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-navigation.json b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-navigation.json index 200c660c..8311da9b 100644 --- a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-navigation.json +++ b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-navigation.json @@ -2,6 +2,7 @@ "pages": [ { "title": "test title", + "type": "Configuration", "layout": { "type": "SingleColumn", "center": { diff --git a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-pages.json b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-pages.json index 49c2bf8c..aeaa2580 100644 --- a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-pages.json +++ b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/invalid/invalid-pages.json @@ -2,6 +2,7 @@ "pages": [ { "title": "Bad items", + "type": "Configuration", "layout": { "type": "SingleColumn", "center": { @@ -50,6 +51,7 @@ }, { "title": "Mismatched single column", + "type": "Configuration", "layout": { "type": "SingleColumn", "left": { @@ -62,6 +64,7 @@ }, { "title": "Mismatched two column", + "type": "Configuration", "layout": { "type": "TwoColumn", "center": { diff --git a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/all-fields.json b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/all-fields.json index 6f57f0d1..35eec378 100644 --- a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/all-fields.json +++ b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/all-fields.json @@ -1,6 +1,7 @@ { "pages": [ { + "type": "Configuration", "backgroundImage": { "name": "SentimentAnalysisBackground", "namespace": "ns" @@ -152,6 +153,7 @@ }, "condition": "{{Variables.foo}}", "title": "Primitive Variables", + "type": "Configuration", "layout": { "type": "TwoColumn", "header": { @@ -235,6 +237,50 @@ ] } } + }, + { + "title": "Validation Page", + "type": "Validation", + "backgroundImage": { + "name": "name", + "namespace": "ns" + }, + "condition": "{{true}}", + "guidancePanel": { + "title": "help", + "backgroundImage": { + "name": "name" + }, + "items": [ + { + "type": "Text", + "text": "help text" + } + ] + }, + "header": { + "text": "Header text", + "description": "header description" + }, + "helpUrl": "https://github.com", + "navigation": { + "label": "Validation", + "parentLabel": "Foo" + }, + "groups": [ + { + "text": "text only" + }, + { + "text": "with tags", + "tags": ["A", "B"] + }, + { + "text": "all the things", + "tags": [], + "includeUnmatched": true + } + ] } ], "displayMessages": [ diff --git a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/empty-items.json b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/empty-items.json index 82451822..9332ff17 100644 --- a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/empty-items.json +++ b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/empty-items.json @@ -2,6 +2,7 @@ "pages": [ { "title": "Page title", + "type": "Configuration", "layout": { "type": "SingleColumn", "center": { @@ -14,6 +15,7 @@ }, { "title": "Page title", + "type": "Configuration", "layout": { "type": "TwoColumn", "left": { diff --git a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/nulls.json b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/nulls.json index 16af0532..aabb4e77 100644 --- a/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/nulls.json +++ b/packages/analyticsdx-template-lint/test/unit/schemas/testfiles/layout/valid/nulls.json @@ -2,6 +2,7 @@ "pages": [ { "title": "", + "type": "Configuration", "backgroundImage": null, "condition": null, "helpUrl": null, @@ -15,6 +16,7 @@ }, { "title": "", + "type": "Configuration", "layout": { "type": "SingleColumn", "header": { @@ -71,6 +73,26 @@ ] } } + }, + { + "title": "Validation page1", + "type": "Validation", + "backgroundImage": null, + "condition": null, + "header": null, + "helpUrl": null, + "groups": null + }, + { + "title": "Validation page2", + "type": "Validation", + "groups": [ + { + "text": "text", + "tags": null, + "includeUnmatched": null + } + ] } ] } diff --git a/packages/analyticsdx-template-lint/test/unit/utils.test.ts b/packages/analyticsdx-template-lint/test/unit/utils.test.ts index c52f7a88..0f6392f6 100644 --- a/packages/analyticsdx-template-lint/test/unit/utils.test.ts +++ b/packages/analyticsdx-template-lint/test/unit/utils.test.ts @@ -8,6 +8,7 @@ import { expect } from 'chai'; import { JSONPath, Node as JsonNode, ParseError, parseTree, printParseErrorCode } from 'jsonc-parser'; import { + caching, fuzzySearcher, isValidRelpath, isValidVariableName, @@ -581,4 +582,36 @@ describe('utils', () => { expect(fuzz(pattern)).has.members([]); }); }); + + describe('caching()', () => { + it('calls underlying function only once', () => { + let count = 0; + const fn = caching((arg: string) => { + count++; + return arg.toLowerCase(); + }); + expect(count).to.equal(0); + // call it first time, should call underlying function + expect(fn('Foo')).to.equal('foo'); + expect(count).to.equal(1); + // call it second time, should use cached value and not call underlying function + expect(fn('Bar')).to.equal('foo'); + expect(count).to.equal(1); + }); + + it('throws exception on subsequent calls', () => { + let count = 0; + const fn = caching((arg: string) => { + count++; + throw new Error('expected'); + }); + // first call should call underlying function and throw error + // (Note: .to.throw() does a string contains by default when passed a string, so using an exact regex) + expect(() => fn('Foo')).to.throw(/^expected$/); + expect(count).to.equal(1); + // second should not call underlying function, and throw same error + expect(() => fn('Foo')).to.throw(/^expected$/); + expect(count).to.equal(1); + }); + }); }); diff --git a/test-assets/sfdx-simple/force-app/main/default/waveTemplates/BadVariables/layout.json b/test-assets/sfdx-simple/force-app/main/default/waveTemplates/BadVariables/layout.json index d003736c..172a8a6e 100644 --- a/test-assets/sfdx-simple/force-app/main/default/waveTemplates/BadVariables/layout.json +++ b/test-assets/sfdx-simple/force-app/main/default/waveTemplates/BadVariables/layout.json @@ -2,6 +2,7 @@ "pages": [ { "title": "Invalid variable types in non-vfPage", + "type": "Configuration", "layout": { "type": "SingleColumn", "center": { diff --git a/test-assets/sfdx-simple/force-app/main/default/waveTemplates/allRelpaths/layout.json b/test-assets/sfdx-simple/force-app/main/default/waveTemplates/allRelpaths/layout.json index a292f0bc..4aa0ac88 100644 --- a/test-assets/sfdx-simple/force-app/main/default/waveTemplates/allRelpaths/layout.json +++ b/test-assets/sfdx-simple/force-app/main/default/waveTemplates/allRelpaths/layout.json @@ -7,6 +7,7 @@ "pages": [ { "title": "Page1", + "type": "Configuration", "layout": { "type": "SingleColumn", "center": { @@ -90,6 +91,7 @@ }, { "title": "Page 2", + "type": "Configuration", "layout": { "type": "TwoColumn", // these are here for checking tiles completions in layout.test.ts @@ -189,6 +191,7 @@ }, { "title": "Page 3", + "type": "Configuration", "layout": { "type": "SingleColumn", "center": { @@ -202,6 +205,16 @@ }, "items": [] } + }, + { + "title": "Validation page", + "type": "Validation", + "groups": [ + { + "text": "group", + "tags": [] + } + ] } ], "displayMessages": [ diff --git a/test-assets/sfdx-simple/force-app/main/default/waveTemplates/allRelpaths/readiness.json b/test-assets/sfdx-simple/force-app/main/default/waveTemplates/allRelpaths/readiness.json index c926895b..9da29e25 100644 --- a/test-assets/sfdx-simple/force-app/main/default/waveTemplates/allRelpaths/readiness.json +++ b/test-assets/sfdx-simple/force-app/main/default/waveTemplates/allRelpaths/readiness.json @@ -7,6 +7,11 @@ "error": "intentional error for tests to look for", "values": {}, - "templateRequirements": [], + "templateRequirements": [ + { + "expression": "{{Variables.string == 'foo'}}", + "tags": ["Tag1", "Tag2"] + } + ], "definition": {} }