UNPKG

@embeddable.com/sdk-core

Version:

Core Embeddable SDK module responsible for web-components bundling and publishing.

1,372 lines (1,251 loc) 42.5 kB
import { clientContextValidation, dataModelsValidation, embeddableValidation, formatIssue, securityContextValidation, } from "./validate"; import * as fs from "node:fs/promises"; const startMock = { succeed: vi.fn(), fail: vi.fn(), }; vi.mock("@embeddable.com/sdk-utils", async (importOriginal) => { const actual = await importOriginal<typeof import("@embeddable.com/sdk-utils")>(); return { ...actual, errorFormatter: vi.fn((issues) => issues.map((issue: any) => issue.message || "Validation error"), ), findFiles: vi.fn(), }; }); const failMock = vi.fn(); const validYaml = `cubes: - name: customers title: My customers data_source: default sql_table: public.customers dimensions: - name: id sql: id type: number primary_key: true`; const invalidYaml = `${validYaml} measures: - name: count type: count title: '# of customers' - name: test type: number sql: {count} / 10.0`; const securityContextYaml = ` - name: Example customer 1 securityContext: country: United States environment: default`; const clientContextYaml = ` - name: blue clientContext: color: blue`; const clientContextWithVariablesYaml = ` - name: US clientContext: theme: default locale: en-US variables: currency: USD country: United States`; const clientContextWithEmptyVariablesYaml = ` - name: UK clientContext: theme: default locale: en-GB variables: {}`; const clientContextWithVariousVariableTypesYaml = ` - name: Complex clientContext: theme: dark variables: stringVar: "hello" numberVar: 42 booleanVar: true arrayVar: [1, 2, 3] objectVar: nested: "value"`; vi.mock("ora", () => ({ default: () => ({ start: vi.fn().mockImplementation(() => startMock), info: vi.fn(), fail: failMock, }), })); vi.mock("node:fs/promises", () => ({ readFile: vi.fn(), writeFile: vi.fn(), access: vi.fn(), mkdir: vi.fn(), })); describe("validate", () => { describe("dataModelsValidation", () => { it("should return an empty array if the data models are valid", async () => { vi.mocked(fs.readFile).mockImplementation(async () => { return validYaml; }); const filesList: [string, string][] = [ ["valid-cube.yaml", "path/to/file"], ]; const result = await dataModelsValidation(filesList); expect(result).toEqual([]); }); it("should return an array of error messages if the data models are invalid", async () => { vi.mocked(fs.readFile).mockImplementation(async () => { return ""; }); const filesList: [string, string][] = [ ["invalid-cube.yaml", "path/to/file"], ]; const result = await dataModelsValidation(filesList); expect(result).toEqual([ "path/to/file: At least one cubes or views must be defined", ]); }); it("should return an array of error messages if the data models parsing fails", async () => { vi.mocked(fs.readFile).mockImplementation(async () => { return invalidYaml; }); const filesList: [string, string][] = [ ["invalid-cube.yaml", "path/to/file"], ]; const result = await dataModelsValidation(filesList); expect(result).toMatchInlineSnapshot(` [ "path/to/file: Unexpected scalar at node end at line 18, column 22: sql: {count} / 10.0 ^^^^^^ ", ] `); }); }); describe("securityContextValidation", () => { it("should return an empty array if the security context is valid", async () => { vi.mocked(fs.readFile).mockImplementation(async () => { return securityContextYaml; }); const filesList: [string, string][] = [ ["valid-security-context.json", "path/to/file"], ]; const result = await securityContextValidation(filesList); expect(result).toEqual([]); }); it("should return an array of error messages if the security context is invalid", async () => { vi.mocked(fs.readFile).mockImplementation(async () => { return `${securityContextYaml} ${securityContextYaml}`; }); const filesList: [string, string][] = [ ["invalid-security-context.json", "path/to/file"], ]; const result = await securityContextValidation(filesList); expect(result).toEqual([ 'path/to/file: security context with name "Example customer 1" already exists', ]); }); }); describe("clientContextValidation", () => { it("should return an empty array if the client context is valid", async () => { vi.mocked(fs.readFile).mockImplementation(async () => { return clientContextYaml; }); const filesList: [string, string][] = [ ["valid-client-context.json", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result).toEqual([]); }); it("should return an array of error messages if the client context is invalid", async () => { vi.mocked(fs.readFile).mockImplementation(async () => { return `${clientContextYaml} ${clientContextYaml}`; }); const filesList: [string, string][] = [ ["invalid-client-context.json", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result).toEqual([ 'path/to/file: client context with name "blue" already exists', ]); }); it("should validate client context with variables field", async () => { vi.mocked(fs.readFile).mockImplementation(async () => { return clientContextWithVariablesYaml; }); const filesList: [string, string][] = [ ["valid-client-context-with-variables.yaml", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result).toEqual([]); }); it("should validate client context with empty variables object", async () => { vi.mocked(fs.readFile).mockImplementation(async () => { return clientContextWithEmptyVariablesYaml; }); const filesList: [string, string][] = [ ["valid-client-context-empty-variables.yaml", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result).toEqual([]); }); it("should validate client context with various variable types", async () => { vi.mocked(fs.readFile).mockImplementation(async () => { return clientContextWithVariousVariableTypesYaml; }); const filesList: [string, string][] = [ ["valid-client-context-various-types.yaml", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result).toEqual([]); }); it("should validate client context without variables field (backward compatibility)", async () => { vi.mocked(fs.readFile).mockImplementation(async () => { return clientContextYaml; }); const filesList: [string, string][] = [ ["valid-client-context-no-variables.yaml", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result).toEqual([]); }); it("should reject client context with variables as non-object type", async () => { const invalidYaml = ` - name: Invalid clientContext: theme: default variables: "not an object"`; vi.mocked(fs.readFile).mockImplementation(async () => { return invalidYaml; }); const filesList: [string, string][] = [ ["invalid-variables-type.yaml", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should reject client context with variables as array", async () => { const invalidYaml = ` - name: Invalid clientContext: theme: default variables: - item1 - item2`; vi.mocked(fs.readFile).mockImplementation(async () => { return invalidYaml; }); const filesList: [string, string][] = [ ["invalid-variables-array.yaml", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should validate client context with variables containing null values", async () => { const yamlWithNull = ` - name: WithNull clientContext: theme: default variables: nullableVar: null stringVar: "value"`; vi.mocked(fs.readFile).mockImplementation(async () => { return yamlWithNull; }); const filesList: [string, string][] = [ ["valid-with-null.yaml", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result).toEqual([]); }); it("should validate multiple client contexts with variables in same file", async () => { const multipleContextsYaml = ` - name: US clientContext: theme: default variables: currency: USD - name: UK clientContext: theme: default variables: currency: GBP`; vi.mocked(fs.readFile).mockImplementation(async () => { return multipleContextsYaml; }); const filesList: [string, string][] = [ ["multiple-contexts.yaml", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result).toEqual([]); }); it("should validate client context with variables having special characters in keys", async () => { const yamlWithSpecialKeys = ` - name: SpecialKeys clientContext: theme: default variables: "key-with-dashes": "value1" "key_with_underscores": "value2" "key.with.dots": "value3" "key with spaces": "value4"`; vi.mocked(fs.readFile).mockImplementation(async () => { return yamlWithSpecialKeys; }); const filesList: [string, string][] = [ ["special-keys.yaml", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result).toEqual([]); }); it("should validate client context with deeply nested variables", async () => { const yamlWithNested = ` - name: Nested clientContext: theme: default variables: level1: level2: level3: "deep value" array: [1, 2, 3] simple: "value"`; vi.mocked(fs.readFile).mockImplementation(async () => { return yamlWithNested; }); const filesList: [string, string][] = [ ["nested-variables.yaml", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result).toEqual([]); }); it("should validate client context with variables and canvas together", async () => { const yamlWithBoth = ` - name: Complete clientContext: theme: default variables: currency: USD canvas: width: 800`; vi.mocked(fs.readFile).mockImplementation(async () => { return yamlWithBoth; }); const filesList: [string, string][] = [ ["complete-context.yaml", "path/to/file"], ]; const result = await clientContextValidation(filesList); expect(result).toEqual([]); }); }); describe("embeddableValidation", () => { const validEmbeddableYaml = ` embeddables: - name: my-embeddable title: My Embeddable variables: - name: date-range type: timeRange array: false defaultValue: from: "2024-01-01" to: "2024-12-31" datasets: - name: filtered data model: daily_listens filters: - member: daily_listens.age_group operator: equals value: date-range valueType: VARIABLE widgets: - component: MultiSelectFieldPro position: x: 0 y: 3 dimensions: width: 7 height: 3 inputs: - input: title inputType: string value: "Age Group" valueType: VALUE - input: ds inputType: dataset valueType: VALUE value: filtered data events: - event: onChange action: SET_VARIABLE config: variable: date-range sourceType: EVENT_PROPERTY sourceValue: value`; const minimalEmbeddableYaml = ` embeddables: - name: minimal-dash title: Minimal`; it("should return an empty array for a valid embeddable file", async () => { vi.mocked(fs.readFile).mockImplementation( async () => validEmbeddableYaml, ); const filesList: [string, string][] = [ ["test.embeddable.yaml", "path/to/test.embeddable.yaml"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([]); }); it("should return an empty array for a minimal embeddable file", async () => { vi.mocked(fs.readFile).mockImplementation( async () => minimalEmbeddableYaml, ); const filesList: [string, string][] = [ ["test.embeddable.yaml", "path/to/test.embeddable.yaml"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([]); }); it("should reject missing root embeddables key", async () => { const yaml = ` name: bad title: Bad`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["bad.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should reject empty embeddables array", async () => { const yaml = ` embeddables: []`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["empty.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should reject unrecognised top-level keys", async () => { const yaml = ` embeddables: - name: test title: Test unknownKey: bad`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["bad.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should reject unrecognised keys on an embeddable item", async () => { const yaml = ` embeddables: - name: test title: Test bogus: true`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["bad.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should detect duplicate embeddable names within a file", async () => { const yaml = ` embeddables: - name: duplicate-name title: First - name: duplicate-name title: Second`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["dup.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([ 'path/to/file: embeddable with name "duplicate-name" already exists', ]); }); it("should detect duplicate embeddable names across files", async () => { const yaml1 = ` embeddables: - name: shared-name title: File One`; const yaml2 = ` embeddables: - name: shared-name title: File Two`; let callCount = 0; vi.mocked(fs.readFile).mockImplementation(async () => { return callCount++ === 0 ? yaml1 : yaml2; }); const filesList: [string, string][] = [ ["a.embeddable.yaml", "path/to/a.embeddable.yaml"], ["b.embeddable.yaml", "path/to/b.embeddable.yaml"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([ 'path/to/b.embeddable.yaml: embeddable with name "shared-name" already exists', ]); }); it("should reject invalid valueType enum values", async () => { const yaml = ` embeddables: - name: test widgets: - component: Comp position: { x: 0, y: 0 } dimensions: { width: 1, height: 1 } inputs: - input: foo inputType: string value: bar valueType: INVALID`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["bad.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should reject invalid action enum values", async () => { const yaml = ` embeddables: - name: test variables: - name: v1 type: string widgets: - component: Comp position: { x: 0, y: 0 } dimensions: { width: 1, height: 1 } events: - event: onClick action: INVALID_ACTION config: variable: v1 sourceType: EVENT_PROPERTY sourceValue: val`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["bad.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should reject invalid sourceType enum values", async () => { const yaml = ` embeddables: - name: test variables: - name: v1 type: string widgets: - component: Comp position: { x: 0, y: 0 } dimensions: { width: 1, height: 1 } events: - event: onClick action: SET_VARIABLE config: variable: v1 sourceType: BADTYPE sourceValue: val`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["bad.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should detect dataset filter referencing undefined variable", async () => { const yaml = ` embeddables: - name: test variables: - name: existing-var type: string datasets: - name: ds1 model: my_model filters: - member: my_model.field operator: equals value: nonexistent-var valueType: VARIABLE`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["ref.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([ 'path/to/file: dataset "ds1" references undefined variable "nonexistent-var"', ]); }); it("should not flag dataset filter with valueType VALUE", async () => { const yaml = ` embeddables: - name: test datasets: - name: ds1 model: my_model filters: - member: my_model.field operator: equals value: some-literal valueType: VALUE`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["ok.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([]); }); it("should detect widget input referencing undefined variable", async () => { const yaml = ` embeddables: - name: test variables: - name: known-var type: string widgets: - component: Comp position: { x: 0, y: 0 } dimensions: { width: 1, height: 1 } inputs: - input: myInput inputType: string value: unknown-var valueType: VARIABLE`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["ref.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([ 'path/to/file: widget "Comp" input "myInput" references undefined variable "unknown-var"', ]); }); it("should detect widget input referencing undefined dataset", async () => { const yaml = ` embeddables: - name: test datasets: - name: real-dataset model: my_model widgets: - component: Comp position: { x: 0, y: 0 } dimensions: { width: 1, height: 1 } inputs: - input: ds inputType: dataset value: fake-dataset valueType: VALUE`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["ref.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([ 'path/to/file: widget "Comp" input "ds" references undefined dataset "fake-dataset"', ]); }); it("should detect SET_VARIABLE event referencing undefined variable", async () => { const yaml = ` embeddables: - name: test variables: - name: real-var type: string widgets: - component: Comp position: { x: 0, y: 0 } dimensions: { width: 1, height: 1 } events: - event: onChange action: SET_VARIABLE config: variable: missing-var sourceType: EVENT_PROPERTY sourceValue: value`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["ref.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([ 'path/to/file: widget "Comp" event "onChange" references undefined variable "missing-var"', ]); }); it("should detect overlapping widgets in an embeddable", async () => { const yaml = ` embeddables: - name: Test-webc widgets: - component: DateRangePickerCustomPro position: { x: 0, y: 0 } dimensions: { width: 4, height: 7 } - component: BarChartDefaultPro position: { x: 0, y: 2 } dimensions: { width: 12, height: 15 }`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["overlap.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([ 'path/to/file: embeddable "Test-webc" widgets "DateRangePickerCustomPro" and "BarChartDefaultPro" overlap. "DateRangePickerCustomPro" occupies x 0-4, y 0-7; "BarChartDefaultPro" occupies x 0-12, y 2-17.', ]); }); it("should allow widgets that touch edges without overlapping", async () => { const yaml = ` embeddables: - name: Test-webc widgets: - component: DateRangePickerCustomPro position: { x: 0, y: 0 } dimensions: { width: 4, height: 7 } - component: BarChartDefaultPro position: { x: 0, y: 7 } dimensions: { width: 12, height: 15 }`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["valid.embeddable.yaml", "path/to/file"], ]; const result = await embeddableValidation(filesList); expect(result).toEqual([]); }); it("should ignore overlap checks for widgets with non-positive dimensions", async () => { const yaml = ` embeddables: - name: Test-webc widgets: - component: EmptyWidthWidget position: { x: 0, y: 0 } dimensions: { width: 0, height: 7 } - component: BarChartDefaultPro position: { x: 0, y: 2 } dimensions: { width: 12, height: 15 }`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["non-positive-dimensions.embeddable.yaml", "path/to/file"], ]; const result = await embeddableValidation(filesList); expect(result).toEqual([]); }); it("should detect customCanvas referencing undefined dataset", async () => { const yaml = ` embeddables: - name: test datasets: - name: real-ds model: my_model customCanvas: datasets: - dataset: bogus-ds`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["ref.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([ 'path/to/file: customCanvas references undefined dataset "bogus-ds"', ]); }); it("should reject template without key", async () => { const yaml = ` embeddables: - name: test customCanvas: templates: - name: Real Template component: SomeComp`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["nokey.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); expect(result[0]).toContain("key"); }); it("should detect duplicate template keys", async () => { const yaml = ` embeddables: - name: test customCanvas: templates: - key: shared name: First component: CompA - key: shared name: Second component: CompB`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["dupkey.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([ 'path/to/file: customCanvas has duplicate template key "shared"', ]); }); it("should resolve starterCanvas reference by template key, not name", async () => { const yaml = ` embeddables: - name: test customCanvas: templates: - key: my-template name: Friendly Display Name component: SomeComp starterCanvas: widgets: - template: my-template dimensions: { width: 7, height: 3 }`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["bykey.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([]); }); it("should detect starterCanvas referencing template by name instead of key", async () => { const yaml = ` embeddables: - name: test customCanvas: templates: - key: my-template name: Friendly Display Name component: SomeComp starterCanvas: widgets: - template: Friendly Display Name dimensions: { width: 7, height: 3 }`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["byname.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([ 'path/to/file: starterCanvas references undefined template "Friendly Display Name"', ]); }); it("should detect starterCanvas referencing undefined template", async () => { const yaml = ` embeddables: - name: test customCanvas: templates: - key: real-template name: Real Template component: SomeComp starterCanvas: widgets: - template: Missing Template dimensions: { width: 7, height: 3 }`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["ref.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([ 'path/to/file: starterCanvas references undefined template "Missing Template"', ]); }); it("should handle YAML parse errors gracefully", async () => { vi.mocked(fs.readFile).mockImplementation( async () => ":\ninvalid: [yaml: {{{", ); const filesList: [string, string][] = [ ["bad.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should validate a full example with customCanvas and templates", async () => { const yaml = ` embeddables: - name: full-example title: Full Example variables: - name: country type: string datasets: - name: main-data model: users widgets: - component: Table position: { x: 0, y: 0 } dimensions: { width: 12, height: 6 } inputs: - input: ds inputType: dataset value: main-data valueType: VALUE events: - event: onClick action: DRILLDOWN config: embeddable: detail-view variableOverrides: - variable: country sourceType: EVENT_PROPERTY sourceValue: chosenItem customCanvas: datasets: - dataset: main-data templates: - key: bar-chart name: Bar Chart component: BarComp description: A bar chart icon: bar_chart inputs: - input: ds value: main-data valueType: VALUE starterCanvas: widgets: - template: bar-chart dimensions: { width: 6, height: 4 }`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["full.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([]); }); it("should accept array: true on a template input", async () => { const yaml = ` embeddables: - name: test customCanvas: templates: - key: my-chart name: My Chart component: ChartComp inputs: - input: ds value: some-ds array: true`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["array.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([]); }); it("should accept visible on a template input", async () => { const yaml = ` embeddables: - name: test customCanvas: templates: - key: my-chart name: My Chart component: ChartComp inputs: - input: ds value: some-ds visible: false - input: title value: Hello visible: true`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["visible.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([]); }); it("should reject non-boolean visible on a template input", async () => { const yaml = ` embeddables: - name: test customCanvas: templates: - key: my-chart name: My Chart component: ChartComp inputs: - input: ds value: some-ds visible: "yes"`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["visible-bad.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should validate dataset filter with input config containing order", async () => { const yaml = ` embeddables: - name: test datasets: - name: ds1 model: my_model widgets: - component: Table position: { x: 0, y: 0 } dimensions: { width: 12, height: 6 } inputs: - input: ds inputType: dataset value: ds1 valueType: VALUE config: filters: [] limit: 10 order: - member: my_model.field direction: asc`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["config.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([]); }); it("should reject invalid order direction", async () => { const yaml = ` embeddables: - name: test widgets: - component: Table position: { x: 0, y: 0 } dimensions: { width: 12, height: 6 } inputs: - input: ds inputType: dataset value: ds1 valueType: VALUE config: order: - member: my_model.field direction: sideways`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["bad.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); }); it("should validate nested config inputs on widgets", async () => { const yaml = ` embeddables: - name: test widgets: - component: DimField position: { x: 0, y: 0 } dimensions: { width: 4, height: 2 } inputs: - input: dim inputType: dimension value: my_model.field valueType: VALUE config: inputs: - input: suffix value: some suffix valueType: VALUE`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["nested.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([]); }); it("should validate input with array flag and config.dataset with parentValue on nested inputs", async () => { const yaml = ` embeddables: - name: my-embeddable-new title: My Embeddable updated variables: - name: date-range type: timeRange array: false defaultValue: { relativeTimeString: 'Last 30 days' } datasets: - name: myDataset model: orders widgets: - component: BarChartDefaultPro position: { x: 0, y: 2 } dimensions: { width: 12, height: 15 } inputs: - input: dataset inputType: dataset valueType: VALUE value: myDataset - input: measures inputType: measure valueType: VALUE array: true value: - customers.count - orders.count config: dataset: dataset inputs: - input: prefix value: "Test 2" parentValue: customers.count valueType: VALUE - input: suffix value: "Suf 1" parentValue: orders.count valueType: VALUE - input: dimension inputType: dimension valueType: VALUE value: orders.created_at config: dataset: dataset inputs: - input: granularity value: "month" valueType: VALUE`; vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["array-parent.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([]); }); it("should accept valid operation values and reject invalid ones on variables", async () => { const validYaml = ` embeddables: - name: test variables: - name: v1 type: string operation: NO_FILTER - name: v2 type: string operation: VALUE`; vi.mocked(fs.readFile).mockImplementation(async () => validYaml); const filesList: [string, string][] = [ ["test.embeddable.yaml", "path/to/file"], ]; expect((await embeddableValidation(filesList)).map(formatIssue)).toEqual( [], ); const invalidYaml = ` embeddables: - name: test variables: - name: v1 type: string operation: INVALID`; vi.mocked(fs.readFile).mockImplementation(async () => invalidYaml); const errors = (await embeddableValidation(filesList)).map(formatIssue); expect(errors.length).toBeGreaterThan(0); expect(errors[0]).toContain("path/to/file"); }); it("should return no errors for an empty file list", async () => { const filesList: [string, string][] = []; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result).toEqual([]); }); it("should reference embeddable name instead of index in error messages", async () => { const yaml = ` embeddables: - name: My Dashboard widgets: - component: BarChart position: { x: 0, y: 0 } dimensions: { width: 4, height: 2 } inputs: - input: showValueLabel value: true valueType: VALUE`; // inputType is required but missing — should name the embeddable and widget vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["bad.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("Embeddable 'My Dashboard'"); expect(result[0]).toContain("widget 'BarChart'"); expect(result[0]).toContain("input 'showValueLabel'"); expect(result[0]).not.toMatch(/embeddables\[0\]/); expect(result[0]).not.toMatch(/widgets\[0\]/); expect(result[0]).not.toMatch(/inputs\[0\]/); }); it("should reference the correct names when error is on a later index", async () => { const yaml = ` embeddables: - name: First Dashboard widgets: - component: PieChart position: { x: 0, y: 0 } dimensions: { width: 4, height: 2 } inputs: - input: title inputType: string value: My Title valueType: VALUE - input: dataset inputType: dataset value: ds1 valueType: VALUE - input: metricLabel value: Revenue valueType: VALUE`; // inputType missing on the third input (index 2) vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["indexed.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("Embeddable 'First Dashboard'"); expect(result[0]).toContain("widget 'PieChart'"); expect(result[0]).toContain("input 'metricLabel'"); expect(result[0]).not.toMatch(/inputs\[2\]/); }); it("should fall back to index notation when name fields are absent in raw data", async () => { // An embeddable with no 'name' field — schema will reject it, but the // formatter should degrade gracefully (e.g. keep embeddables[0]). const yaml = ` embeddables: - title: Unnamed`; // 'name' is required, so there will be a validation error at embeddables[0] vi.mocked(fs.readFile).mockImplementation(async () => yaml); const filesList: [string, string][] = [ ["noname.embeddable.yaml", "path/to/file"], ]; const result = (await embeddableValidation(filesList)).map(formatIssue); expect(result.length).toBeGreaterThan(0); expect(result[0]).toContain("path/to/file"); // Should not crash — just degrade to index notation expect(result[0]).toMatch(/embeddables\[0\]/); }); }); });