UNPKG

@flourish/sdk

Version:
1,142 lines (1,077 loc) 53.6 kB
const assert = require("assert"), fs = require("fs"), nodeResolve = require("resolve"), path = require("path"), sinon = require("sinon"); const validateConfig = require("../../lib/validate_config"); describe("validate_config", function() { let temp_directory; before(async function() { const { temporaryDirectory } = await import("tempy"); temp_directory = temporaryDirectory(); fs.mkdirSync(path.join(temp_directory, "data")); fs.openSync(path.join(temp_directory, "data", "Foo.csv"), "w"); }); function expectFailure(config, expected_message) { try { validateConfig(config, temp_directory); } catch (e) { assert.equal(e.message, expected_message); return; } assert.fail(`Got no error; expected “${expected_message}”`); } function expectSuccess(config) { validateConfig(config, temp_directory); } const id = "best-template", name = "Best template", author = "Robin Houston", sdk_version = 3; const metadata = { id, name, author, sdk_version }; const binding = { name: "My binding", dataset: "dataset", key: "key", type: "column", column: "Foo::A" }; const setting_foo = { name: "Foo", property: "foo", type: "string" }; const setting_bar = { name: "Bar", property: "bar", type: "string" }; function metadataPlus(o) { return Object.assign({}, metadata, o); } function bindingPlus(o) { return metadataPlus({ data: [Object.assign({}, binding, o)] }); } function settingPlus(o, other, more_settings=[]) { return metadataPlus(Object.assign({ settings: [Object.assign({}, setting_foo, o), setting_bar].concat(more_settings) }, other)); } function testBoolean(name) { return function() { it(`should accept ${name}=true`, function() { expectSuccess( metadataPlus({ [name]: true }), ); }); it(`should accept ${name}=false`, function() { expectSuccess( metadataPlus({ [name]: false }), ); }); it(`should reject ${name}=null`, function() { expectFailure( metadataPlus({ [name]: null }), `template.yml: Bad ${name} setting; must be either true or false`, ); }); it(`should reject ${name}=undefined`, function() { expectFailure( metadataPlus({ [name]: undefined }), `template.yml: Bad ${name} setting; must be either true or false`, ); }); it(`should reject strings for ${name}`, function() { expectFailure( metadataPlus({ [name]: "false" }), `template.yml: Bad ${name} setting; must be either true or false`, ); }); it(`should reject numbers for ${name}`, function() { expectFailure( metadataPlus({ [name]: 1 }), `template.yml: Bad ${name} setting; must be either true or false`, ); }); it(`should reject objects for ${name}`, function() { expectFailure( metadataPlus({ [name]: {} }), `template.yml: Bad ${name} setting; must be either true or false`, ); }); }; } function testObject(name, expected_error_message) { return function() { it(`should reject ${name}=true`, function() { expectFailure( metadataPlus({ [name]: true }), expected_error_message, ); }); it(`should reject ${name}=false`, function() { expectFailure( metadataPlus({ [name]: false }), expected_error_message, ); }); it(`should reject ${name}=null`, function() { expectFailure( metadataPlus({ [name]: null }), expected_error_message, ); }); it(`should reject ${name}=undefined`, function() { expectFailure( metadataPlus({ [name]: undefined }), expected_error_message, ); }); it(`should reject strings for ${name}`, function() { expectFailure( metadataPlus({ [name]: "false" }), expected_error_message, ); }); it(`should reject numbers for ${name}`, function() { expectFailure( metadataPlus({ [name]: 1 }), expected_error_message, ); }); it(`should accept objects for ${name}`, function() { expectSuccess( metadataPlus({ [name]: {} }), ); }); }; } describe("top level", function() { it("should reject null", function() { expectFailure(null, "template.yml must define a mapping"); }); it("should reject undefined", function() { expectFailure(undefined, "template.yml must define a mapping"); }); it("should reject a number", function() { expectFailure(23, "template.yml must define a mapping"); }); it("should reject a string", function() { expectFailure("foo", "template.yml must define a mapping"); }); it("should reject an array", function() { expectFailure([], "template.yml must define a mapping"); }); it("should reject true", function() { expectFailure(true, "template.yml must define a mapping"); }); it("should reject false", function() { expectFailure(false, "template.yml must define a mapping"); }); }); describe("metadata", function() { it("should require id", function() { expectFailure( { name, author, sdk_version }, "template.yml must specify an id:", ); }); it("should require name", function() { expectFailure( { id, author, sdk_version }, "template.yml must specify a name:", ); }); it("should require author", function() { expectFailure( { id, name, sdk_version }, "template.yml must specify an author:", ); }); it("should require sdk_version", function() { expectFailure( { id, name, author }, "template.yml must specify an sdk_version:", ); }); // The SDK version number is checked separately, by the checkTemplateVersion function // in lib/sdk.js, which is called on build. }); describe("is_master_slide", testBoolean("is_master_slide")); describe("credits", function() { it("should accept string credits", function() { expectSuccess(metadataPlus({ credits: "" })); expectSuccess(metadataPlus({ credits: "Credits here" })); }); it("should reject null credits", function() { expectFailure(metadataPlus({ credits: null }), "template.yml: Credits must be a string"); }); it("should reject undefined credits", function() { expectFailure(metadataPlus({ credits: undefined }), "template.yml: Credits must be a string"); }); it("should reject numeric credits", function() { expectFailure(metadataPlus({ credits: 23 }), "template.yml: Credits must be a string"); }); it("should reject a credits object", function() { expectFailure(metadataPlus({ credits: {} }), "template.yml: Credits must be a string"); }); it("should reject a credits array", function() { expectFailure(metadataPlus({ credits: [] }), "template.yml: Credits must be a string"); }); }); describe("autoheight", function() { it("should reject autoheight", function() { expectFailure(metadataPlus({ autoheight: "*" }), "template.yml: autoheight is no longer supported. You can use `Flourish.setHeight()` to dynamically adjust the height, if needed."); }); }); describe("image_download", testBoolean("image_download")); describe("svg_download", testBoolean("svg_download")); describe("allowed_standalone_download_origins", function() { it("should accept an empty array", function() { expectSuccess(metadataPlus({ allowed_standalone_download_origins: [] })); }); it("should accept a single URL", function() { expectSuccess(metadataPlus({ allowed_standalone_download_origins: ["https://example.com"] })); }); it("should accept multiple URLs", function() { expectSuccess(metadataPlus({ allowed_standalone_download_origins: [ "https://tiles.flourish.studio", "https://icons.flourish.studio", "http://legacy-api.example.com", ] })); }); it("should reject null", function() { expectFailure(metadataPlus({ allowed_standalone_download_origins: null }), "template.yml: “allowed_standalone_download_origins” must be an array"); }); it("should reject undefined", function() { expectFailure(metadataPlus({ allowed_standalone_download_origins: undefined }), "template.yml: “allowed_standalone_download_origins” must be an array"); }); it("should reject a string", function() { expectFailure(metadataPlus({ allowed_standalone_download_origins: "https://example.com" }), "template.yml: “allowed_standalone_download_origins” must be an array"); }); it("should reject a number", function() { expectFailure(metadataPlus({ allowed_standalone_download_origins: 23 }), "template.yml: “allowed_standalone_download_origins” must be an array"); }); it("should reject an object", function() { expectFailure(metadataPlus({ allowed_standalone_download_origins: {} }), "template.yml: “allowed_standalone_download_origins” must be an array"); }); it("should reject array with non-string entry", function() { expectFailure(metadataPlus({ allowed_standalone_download_origins: ["https://example.com", 123] }), "template.yml: allowed_standalone_download_origins entry “123” is not a string"); }); it("should reject URL without http:// or https://", function() { expectFailure(metadataPlus({ allowed_standalone_download_origins: ["example.com"] }), "template.yml: allowed_standalone_download_origins entry “example.com” must be a valid HTTP(S) URL"); }); it("should reject URL with invalid protocol", function() { expectFailure(metadataPlus({ allowed_standalone_download_origins: ["ftp://example.com"] }), "template.yml: allowed_standalone_download_origins entry “ftp://example.com” must be a valid HTTP(S) URL"); }); }); describe("build rules", function() { testObject("build_rules", "template.yml “build” must be a mapping"); it("should reject null", function() { expectFailure(metadataPlus({ build: { foo: null } }), "template.yml: build rule “foo” is null"); }); it("should reject undefined", function() { expectFailure(metadataPlus({ build: { foo: undefined } }), "template.yml: build rule “foo” is null"); }); it("should reject arrays", function() { expectFailure(metadataPlus({ build: { foo: [] } }), "template.yml: build rule “foo” must be a mapping"); }); it("should reject numbers", function() { expectFailure(metadataPlus({ build: { foo: 23 } }), "template.yml: build rule “foo” must be a mapping"); }); it("should reject true", function() { expectFailure(metadataPlus({ build: { foo: true } }), "template.yml: build rule “foo” must be a mapping"); }); it("should reject false", function() { expectFailure(metadataPlus({ build: { foo: false } }), "template.yml: build rule “foo” must be a mapping"); }); it("should expect a script", function() { expectFailure(metadataPlus({ build: { foo: { } } }), "template.yml: build rule “foo” has no “script”"); }); it("should reject a null script", function() { expectFailure(metadataPlus({ build: { foo: { script: null } } }), "template.yml: build.foo.script must be a string"); }); it("should reject a numeric script", function() { expectFailure(metadataPlus({ build: { foo: { script: 23 } } }), "template.yml: build.foo.script must be a string"); }); it("should reject a true script", function() { expectFailure(metadataPlus({ build: { foo: { script: true } } }), "template.yml: build.foo.script must be a string"); }); it("should reject a false script", function() { expectFailure(metadataPlus({ build: { foo: { script: false } } }), "template.yml: build.foo.script must be a string"); }); it("should expect directory, files or watch", function() { expectFailure(metadataPlus({ build: { foo: { script: "" } } }), "template.yml: build rule “foo” has no “directory”, “files” or “watch”"); }); it("should accept a directory alone", function() { expectSuccess(metadataPlus({ build: { foo: { script: "", directory: "." } } })); }); it("should accept a files array alone", function() { expectSuccess(metadataPlus({ build: { foo: { script: "", files: [] } } })); }); it("should accept a watch command alone", function() { expectSuccess(metadataPlus({ build: { foo: { script: "", watch: "" } } })); }); it("should reject a watch command and a directory", function() { expectFailure(metadataPlus({ build: { foo: { script: "", watch: "", directory: "" } } }), "template.yml: build rule “foo” has both “watch” and “directory”"); }); it("should reject a watch command and a list of files", function() { expectFailure(metadataPlus({ build: { foo: { script: "", watch: "", files: [""] } } }), "template.yml: build rule “foo” has both “watch” and files"); }); it("should reject null files", function() { expectFailure(metadataPlus({ build: { foo: { script: "", files: null } } }), "template.yml: “build.foo.files” must be an array"); }); it("should reject numeric files", function() { expectFailure(metadataPlus({ build: { foo: { script: "", files: 23 } } }), "template.yml: “build.foo.files” must be an array"); }); it("should reject true files", function() { expectFailure(metadataPlus({ build: { foo: { script: "", files: true } } }), "template.yml: “build.foo.files” must be an array"); }); it("should reject false files", function() { expectFailure(metadataPlus({ build: { foo: { script: "", files: false } } }), "template.yml: “build.foo.files” must be an array"); }); it("should reject a files object", function() { expectFailure(metadataPlus({ build: { foo: { script: "", files: {} } } }), "template.yml: “build.foo.files” must be an array"); }); it("should reject a null file", function() { expectFailure(metadataPlus({ build: { foo: { script: "", files: [null] } } }), "template.yml: the entries of “build.foo.files” must be strings"); }); it("should reject an undefined file", function() { expectFailure(metadataPlus({ build: { foo: { script: "", files: [undefined] } } }), "template.yml: the entries of “build.foo.files” must be strings"); }); it("should reject a numeric file", function() { expectFailure(metadataPlus({ build: { foo: { script: "", files: [23] } } }), "template.yml: the entries of “build.foo.files” must be strings"); }); it("should reject a true file", function() { expectFailure(metadataPlus({ build: { foo: { script: "", files: [true] } } }), "template.yml: the entries of “build.foo.files” must be strings"); }); it("should reject a false file", function() { expectFailure(metadataPlus({ build: { foo: { script: "", files: [false] } } }), "template.yml: the entries of “build.foo.files” must be strings"); }); it("should accept a string file", function() { expectSuccess(metadataPlus({ build: { foo: { script: "", files: [""] } } })); }); it("should accept two strings", function() { expectSuccess(metadataPlus({ build: { foo: { script: "", files: ["one", "two"] } } })); }); it("should reject a null directory", function() { expectFailure(metadataPlus({ build: { foo: { script: "", directory: null } } }), "template.yml: “build.foo.directory” must be a string"); }); it("should reject an undefined directory", function() { expectFailure(metadataPlus({ build: { foo: { script: "", directory: undefined } } }), "template.yml: “build.foo.directory” must be a string"); }); it("should reject a numeric directory", function() { expectFailure(metadataPlus({ build: { foo: { script: "", directory: 23 } } }), "template.yml: “build.foo.directory” must be a string"); }); it("should reject a true directory", function() { expectFailure(metadataPlus({ build: { foo: { script: "", directory: true } } }), "template.yml: “build.foo.directory” must be a string"); }); it("should reject a false directory", function() { expectFailure(metadataPlus({ build: { foo: { script: "", directory: false } } }), "template.yml: “build.foo.directory” must be a string"); }); it("should accept a string directory", function() { expectSuccess(metadataPlus({ build: { foo: { script: "", directory: "" } } })); }); }); describe("data bindings", function() { it("should accept null", function() { expectSuccess(metadataPlus({ data: null })); }); it("should reject a number", function() { expectFailure(metadataPlus({ data: 23 }), "template.yml: “data” must be an array"); }); it("should reject a string", function() { expectFailure(metadataPlus({ data: "bar" }), "template.yml: “data” must be an array"); }); it("should reject true", function() { expectFailure(metadataPlus({ data: true }), "template.yml: “data” must be an array"); }); it("should reject false", function() { expectFailure(metadataPlus({ data: false }), "template.yml: “data” must be an array"); }); it("should reject an object", function() { expectFailure(metadataPlus({ data: {} }), "template.yml: “data” must be an array"); }); it("should accept an empty array", function() { expectSuccess(metadataPlus({ data: [] })); }); const name = binding.name, dataset = binding.dataset, key = binding.key, type = binding.type; it("should require name", function() { expectFailure(metadataPlus({ data: [{ dataset, key, type }] }), "template.yml data binding must specify a name"); }); it("should require dataset", function() { expectFailure(metadataPlus({ data: [{ name, key, type }] }), "template.yml data binding “My binding” must specify a dataset"); }); it("should require key", function() { expectFailure(metadataPlus({ data: [{ name, dataset, type }] }), "template.yml data binding “My binding” must specify a key"); }); it("should require type", function() { expectFailure(metadataPlus({ data: [{ name, dataset, key }] }), "template.yml data binding “My binding” must specify a type"); }); it("should require column for non-optional column bindings", function() { expectFailure(metadataPlus({ data: [{ name, dataset, key, type }] }), "template.yml non-optional data binding “My binding” must specify column"); }); it("should accept name/dataset/key/type=column/column", function() { expectSuccess(bindingPlus()); }); it("should reject non-existent data tables", function() { expectFailure(bindingPlus({ column: "NoSuchTable::A" }), "template.yml: data binding refers to “NoSuchTable::A”, but data file does not exist"); }); it("should reject multiple columns", function() { expectFailure(bindingPlus({ column: "Foo::A,B" }), "You can only select one column"); }); it("should reject no columns if non-optional", function() { expectFailure(bindingPlus({ column: "Foo::" }), "Non-optional data binding must specify column"); }); it("should accept no columns if optional", function() { expectSuccess(bindingPlus({ column: "Foo::", optional: true })); }); it("should reject a null column", function() { expectFailure(bindingPlus({ column: null }), "template.yml: “column” property of data binding “My binding” must be a string"); }); it("should reject an undefined column", function() { expectFailure(bindingPlus({ column: undefined }), "template.yml: “column” property of data binding “My binding” must be a string"); }); it("should reject a numeric column", function() { expectFailure(bindingPlus({ column: 23 }), "template.yml: “column” property of data binding “My binding” must be a string"); }); it("should reject an object column", function() { expectFailure(bindingPlus({ column: {} }), "template.yml: “column” property of data binding “My binding” must be a string"); }); it("should reject a true column", function() { expectFailure(bindingPlus({ column: true }), "template.yml: “column” property of data binding “My binding” must be a string"); }); it("should reject a false column", function() { expectFailure(bindingPlus({ column: false }), "template.yml: “column” property of data binding “My binding” must be a string"); }); it("should reject a null columns", function() { expectFailure(bindingPlus({ type: "columns", columns: null }), "template.yml: “columns” property of data binding “My binding” must be a string"); }); it("should reject an undefined columns", function() { expectFailure(bindingPlus({ type: "columns", columns: undefined }), "template.yml: “columns” property of data binding “My binding” must be a string"); }); it("should reject a numeric columns", function() { expectFailure(bindingPlus({ type: "columns", columns: 23 }), "template.yml: “columns” property of data binding “My binding” must be a string"); }); it("should reject an object columns", function() { expectFailure(bindingPlus({ type: "columns", columns: {} }), "template.yml: “columns” property of data binding “My binding” must be a string"); }); it("should reject a true columns", function() { expectFailure(bindingPlus({ type: "columns", columns: true }), "template.yml: “columns” property of data binding “My binding” must be a string"); }); it("should reject a false columns", function() { expectFailure(bindingPlus({ type: "columns", columns: false }), "template.yml: “columns” property of data binding “My binding” must be a string"); }); it("should reject null for “optional”", function() { expectFailure(bindingPlus({ optional: null }), "template.yml “optional” property of data binding “My binding” must be a boolean"); }); it("should reject undefined for “optional”", function() { expectFailure(bindingPlus({ optional: undefined }), "template.yml “optional” property of data binding “My binding” must be a boolean"); }); it("should reject numbers for “optional”", function() { expectFailure(bindingPlus({ optional: 123 }), "template.yml “optional” property of data binding “My binding” must be a boolean"); }); it("should reject strings for “optional”", function() { expectFailure(bindingPlus({ optional: "bar" }), "template.yml “optional” property of data binding “My binding” must be a boolean"); }); it("should reject objects for “optional”", function() { expectFailure(bindingPlus({ optional: {} }), "template.yml “optional” property of data binding “My binding” must be a boolean"); }); it("should reject arrays for “optional”", function() { expectFailure(bindingPlus({ optional: [] }), "template.yml “optional” property of data binding “My binding” must be a boolean"); }); it("should accept true for “optional”", function() { expectSuccess(bindingPlus({ optional: true })); }); it("should accept false for “optional”", function() { expectSuccess(bindingPlus({ optional: false })); }); it("should require column if optional is false", function() { expectFailure(metadataPlus({ data: [{ name, dataset, key, type, optional: false }] }), "template.yml non-optional data binding “My binding” must specify column"); }); it("should reject duplicates", function() { expectFailure(metadataPlus({ data: [binding, binding] }), "template.yml: there is more than one data binding with dataset “dataset” and key “key”"); }); it("should ignore headings", function() { expectSuccess(metadataPlus({ data: ["Heading", binding] })); }); describe("cacheable_for_standalone_downloads", function() { it("should accept true", function() { expectSuccess(bindingPlus({ cacheable_for_standalone_downloads: true })); }); it("should accept false", function() { expectSuccess(bindingPlus({ cacheable_for_standalone_downloads: false })); }); it("should reject null", function() { expectFailure(bindingPlus({ cacheable_for_standalone_downloads: null }), "template.yml “cacheable_for_standalone_downloads” property of data binding “My binding” must be a boolean"); }); it("should reject undefined", function() { expectFailure(bindingPlus({ cacheable_for_standalone_downloads: undefined }), "template.yml “cacheable_for_standalone_downloads” property of data binding “My binding” must be a boolean"); }); it("should reject strings", function() { expectFailure(bindingPlus({ cacheable_for_standalone_downloads: "true" }), "template.yml “cacheable_for_standalone_downloads” property of data binding “My binding” must be a boolean"); }); it("should reject numbers", function() { expectFailure(bindingPlus({ cacheable_for_standalone_downloads: 123 }), "template.yml “cacheable_for_standalone_downloads” property of data binding “My binding” must be a boolean"); }); it("should reject arrays", function() { expectFailure(bindingPlus({ cacheable_for_standalone_downloads: [] }), "template.yml “cacheable_for_standalone_downloads” property of data binding “My binding” must be a boolean"); }); it("should reject objects", function() { expectFailure(bindingPlus({ cacheable_for_standalone_downloads: {} }), "template.yml “cacheable_for_standalone_downloads” property of data binding “My binding” must be a boolean"); }); }); }); describe("settings", function() { it("should accept null", function() { expectSuccess(metadataPlus({ settings: null })); }); it("should reject a number", function() { expectFailure(metadataPlus({ settings: 23 }), "template.yml: “settings” must be an array"); }); it("should reject a string", function() { expectFailure(metadataPlus({ settings: "bar" }), "template.yml: “settings” must be an array"); }); it("should reject true", function() { expectFailure(metadataPlus({ settings: true }), "template.yml: “settings” must be an array"); }); it("should reject false", function() { expectFailure(metadataPlus({ settings: false }), "template.yml: “settings” must be an array"); }); it("should reject an object", function() { expectFailure(metadataPlus({ settings: {} }), "template.yml: “settings” must be an array"); }); it("should accept an empty array", function() { expectSuccess(metadataPlus({ settings: [] })); }); it("should require property", function() { expectFailure(metadataPlus({ settings: [{ name: "Foo", type: "string" }] }), "template.yml setting must specify a property:"); }); it("should require type", function() { expectFailure(metadataPlus({ settings: [{ name: "Foo", property: "foo" }] }), "template.yml setting “foo” must specify a type:"); }); it("should require name", function() { expectFailure(metadataPlus({ settings: [{ property: "foo", type: "string" }] }), "template.yml setting “foo” must specify a name:"); }); it("should not require name if choices are specified", function() { expectSuccess(metadataPlus({ settings: [{ property: "foo", type: "string", choices: [] }] })); }); describe("optional settings", function() { it("should not support optional strings", function() { expectFailure(settingPlus({ optional: true }), "The “optional” property is only supported for “number”, “color” and “font” type settings"); }); it("should not support optional booleans", function() { expectFailure(settingPlus({ type: "boolean", optional: true }), "The “optional” property is only supported for “number”, “color” and “font” type settings"); }); it("should support optional numbers", function() { expectSuccess(settingPlus({ type: "number", optional: true })); }); it("should support optional fonts", function() { expectSuccess(settingPlus({ type: "font", optional: true })); }); it("should support non-optional numbers", function() { expectSuccess(settingPlus({ type: "number", optional: false })); }); it("should not recognise optional: null", function() { expectFailure(settingPlus({ type: "number", optional: null }), "template.yml setting “foo” has an invalid value for “optional”: should be true or false"); }); it("should not recognise optional: undefined", function() { expectFailure(settingPlus({ type: "number", optional: undefined }), "template.yml setting “foo” has an invalid value for “optional”: should be true or false"); }); it("should not recognise optional: 23", function() { expectFailure(settingPlus({ type: "number", optional: 23 }), "template.yml setting “foo” has an invalid value for “optional”: should be true or false"); }); it("should not recognise optional: \"bar\"", function() { expectFailure(settingPlus({ type: "number", optional: "bar" }), "template.yml setting “foo” has an invalid value for “optional”: should be true or false"); }); it("should not recognise optional: []", function() { expectFailure(settingPlus({ type: "number", optional: [] }), "template.yml setting “foo” has an invalid value for “optional”: should be true or false"); }); it("should not recognise optional: {}", function() { expectFailure(settingPlus({ type: "number", optional: {} }), "template.yml setting “foo” has an invalid value for “optional”: should be true or false"); }); }); describe("choices", function() { it("should not support choices for a number setting", function() { expectFailure(settingPlus({ type: "number", choices: [] }), "template.yml setting “foo” has a “choices” field, but is of type number"); }); it("should support choices for a string setting", function() { expectSuccess(settingPlus({ choices: [] })); }); it("should not support choices_other without choices", function() { expectFailure(settingPlus({ choices_other: true }), "template.yml setting “foo” has a “choices_other” field, but no choices:"); }); it("should support choices_other with choices", function() { expectSuccess(settingPlus({ choices: [], choices_other: true })); }); it("should not support choices_other: null", function() { expectFailure(settingPlus({ choices: [], choices_other: null }), "template.yml setting “foo” has invalid value for “choices_other”: should be boolean"); }); it("should not support choices_other: 23", function() { expectFailure(settingPlus({ choices: [], choices_other: 23 }), "template.yml setting “foo” has invalid value for “choices_other”: should be boolean"); }); it("should not support choices_other: \"bar\"", function() { expectFailure(settingPlus({ choices: [], choices_other: "bar" }), "template.yml setting “foo” has invalid value for “choices_other”: should be boolean"); }); it("should not support choices_other: []", function() { expectFailure(settingPlus({ choices: [], choices_other: [] }), "template.yml setting “foo” has invalid value for “choices_other”: should be boolean"); }); it("should not support choices_other: {}", function() { expectFailure(settingPlus({ choices: [], choices_other: {} }), "template.yml setting “foo” has invalid value for “choices_other”: should be boolean"); }); it("should support a string array", function() { expectSuccess(settingPlus({ choices: ["a", "b", "c"] })); }); it("should reject a string array if type is not string", function() { expectFailure(settingPlus({ type: "boolean", choices: ["a", "b", "c"] }), "template.yml setting “foo” has a “choices” field with a string element, but is of type boolean"); }); it("should support pairs", function() { expectSuccess(settingPlus({ choices: [ ["A", "a"], ["A", "b"], ["A", "c"], ] })); }); it("should reject singletons", function() { expectFailure(settingPlus({ choices: [ ["A", "a"], ["b"], ["A", "c"], ] }), "template.yml setting “foo”: element 1 of “choices” field has 1 elements (should be 2)"); }); it("should reject numbers for a string setting", function() { expectFailure(settingPlus({ choices: [ ["A", "a"], ["B", 23], ["A", "c"], ] }), "template.yml setting “foo”: second entry of element 1 of “choices” field is not a string"); }); it("should reject true for a string setting", function() { expectFailure(settingPlus({ choices: [ ["A", "a"], ["B", true], ["A", "c"], ] }), "template.yml setting “foo”: second entry of element 1 of “choices” field is not a string"); }); it("should reject false for a string setting", function() { expectFailure(settingPlus({ choices: [ ["A", "a"], ["B", false], ["A", "c"], ] }), "template.yml setting “foo”: second entry of element 1 of “choices” field is not a string"); }); it("should reject null for a string setting", function() { expectFailure(settingPlus({ choices: [ ["A", "a"], ["B", null], ["A", "c"], ] }), "template.yml setting “foo”: second entry of element 1 of “choices” field is not a string"); }); it("should support triples", function() { expectSuccess(settingPlus({ choices: [ ["A", "a"], ["A", "b", "b_image.png"], ["A", "c"], ] })); }); describe("boolean choices", function() { it("should support boolean pairs", function() { expectSuccess(settingPlus({ type: "boolean", choices: [ ["A", true], ["A", false], ] })); }); it("should support boolean pairs the other way round", function() { expectSuccess(settingPlus({ type: "boolean", choices: [ ["A", false], ["A", true], ] })); }); it("should reject an empty list", function() { expectFailure(settingPlus({ type: "boolean", choices: [] }), "template.yml setting “foo”: “choices” field for boolean property can only contain one “false” and one “true” option"); }); it("should reject true alone", function() { expectFailure(settingPlus({ type: "boolean", choices: [ ["A", true], ] }), "template.yml setting “foo”: “choices” field for boolean property can only contain one “false” and one “true” option"); }); it("should reject false alone", function() { expectFailure(settingPlus({ type: "boolean", choices: [ ["A", false], ] }), "template.yml setting “foo”: “choices” field for boolean property can only contain one “false” and one “true” option"); }); it("should reject repetitions", function() { expectFailure(settingPlus({ type: "boolean", choices: [ ["A", true], ["B", true], ["C", false], ] }), "template.yml setting “foo”: “choices” field for boolean property can only contain one “false” and one “true” option"); }); }); }); describe("show_if / hide_if", function() { it("should permit a reference to another setting (show_if)", function() { expectSuccess(settingPlus({ show_if: "bar" })); }); it("should permit a reference to another setting (hide_if)", function() { expectSuccess(settingPlus({ hide_if: "bar" })); }); it("should forbid a reference to itself (show_if shortform)", function() { expectFailure(settingPlus({ show_if: "foo" }), "template.yml setting “foo” cannot be conditional on itself"); }); it("should forbid a reference to itself (hide_if shortform)", function() { expectFailure(settingPlus({ hide_if: "foo" }), "template.yml setting “foo” cannot be conditional on itself"); }); it("should forbid a reference to itself (show_if)", function() { expectFailure(settingPlus({ show_if: { "foo": true } }), "template.yml setting “foo” cannot be conditional on itself"); }); it("should forbid a reference to itself (hide_if)", function() { expectFailure(settingPlus({ hide_if: { "foo": true } }), "template.yml setting “foo” cannot be conditional on itself"); }); it("should forbid a reference to a non-existent setting (show_if)", function() { expectFailure(settingPlus({ show_if: "baz" }), "template.yml: “show_if” or “hide_if” property refers to non-existent setting “baz”"); }); it("should forbid a reference to a non-existent setting (hide_if)", function() { expectFailure(settingPlus({ hide_if: "baz" }), "template.yml: “show_if” or “hide_if” property refers to non-existent setting “baz”"); }); it("should only allow one", function() { expectFailure(settingPlus({ show_if: "bar", hide_if: "bar" }), "template.yml setting “foo” has both “show_if” and “hide_if” properties: there can only be one"); }); it("should forbid null", function() { expectFailure(settingPlus({ show_if: null }), "template.yml Conditional setting “foo” is badly formed or wrongly indented"); }); it("should forbid true", function() { expectFailure(settingPlus({ show_if: true }), "template.yml setting “foo” has a “show_if” value that is not a string or object"); }); it("should forbid false", function() { expectFailure(settingPlus({ show_if: false }), "template.yml setting “foo” has a “show_if” value that is not a string or object"); }); it("should forbid empty arrays", function() { expectFailure(settingPlus({ show_if: [] }), "template.yml setting “foo” “show_if” property must specify a setting to test against"); }); it("should accept an array containing an object with multiple string values", function() { const blah = { name: "Blah", property: "blah", type: "string" }; expectSuccess(settingPlus({ show_if: [{ "bar": "xxx", "blah": "xxx" }] }, undefined, [blah])); }); it("should accept an array containing an object with multiple array values", function() { const blah = { name: "Blah", property: "blah", type: "string" }; expectSuccess(settingPlus({ show_if: [{ "bar": ["xxx"], "blah": ["xxx"] }] }, undefined, [blah])); }); it("should reject an array containing any objects with empty array values", function() { const blah = { name: "Blah", property: "blah", type: "string" }; expectFailure(settingPlus({ show_if: [{ "bar": [], "blah": "xxx" }] }, undefined, [blah]), "template.yml setting “foo” “show_if” property: condition for bar is empty"); }); it("should reject an array containing any objects referring to non-existent settings", function() { expectFailure(settingPlus({ show_if: [{ "bar": "xxx", "baz": "xxx" }] }), "template.yml: “show_if” or “hide_if” property refers to non-existent setting “baz”"); }); it("should accept an array containing an object with a string value and a valid data binding", function() { expectSuccess(settingPlus({ show_if: [{ "bar": "xxx", "data.dataset.key": true }] }, { data: [binding] })); }); it("should reject an array with a data reference when there are no data bindings", function() { expectFailure(settingPlus({ show_if: [{ "bar": "xxx", "data.dataset.key": true }] }), "template.yml: “show_if” or “hide_if” property refers to data binding “data.dataset.key” when none are defined"); }); it("should reject an array with a reference to a non-existent data binding", function() { expectFailure(settingPlus({ show_if: [{ "bar": "xxx", "data.dataset.nosuchkey": true }] }, { data: [binding] }), "template.yml: “show_if” or “hide_if” property refers to non-existent data binding “data.dataset.nosuchkey”"); }); it("should accept an array containing multiple objects of including string and boolean values", function() { const blah = { name: "Blah", property: "blah", type: "string" }; const bool = { name: "Bool", property: "bool", type: "boolean" }; expectSuccess(settingPlus({ show_if: [{ "bar": "xxx", "blah": "xxx" }, { "bool": true }] }, undefined, [blah, bool])); }); it("should forbid empty objects", function() { expectFailure(settingPlus({ show_if: {} }), "template.yml setting “foo” “show_if” property must specify a setting to test against"); }); it("should accept an object with a string value", function() { expectSuccess(settingPlus({ show_if: { "bar": "xxx" } })); }); it("should accept an object with an array value", function() { expectSuccess(settingPlus({ show_if: { "bar": ["xxx"] } })); }); it("should reject an object with an empty array value", function() { expectFailure(settingPlus({ show_if: { "bar": [] } }), "template.yml setting “foo” “show_if” property: value for bar is empty"); }); it("should reject an object referring to a non-existent setting", function() { expectFailure(settingPlus({ show_if: { "baz": ["xxx"] } }), "template.yml: “show_if” or “hide_if” property refers to non-existent setting “baz”"); }); it("should accept a reference to a data binding", function() { expectSuccess(settingPlus({ show_if: "data.dataset.key" }, { data: [binding] })); }); it("should reject a data reference when there are no data bindings", function() { expectFailure(settingPlus({ show_if: "data.dataset.key" }), "template.yml: “show_if” or “hide_if” property refers to data binding “data.dataset.key” when none are defined"); }); it("should reject a reference to a non-existent data binding", function() { expectFailure(settingPlus({ show_if: "data.dataset.nosuchkey" }, { data: [binding] }), "template.yml: “show_if” or “hide_if” property refers to non-existent data binding “data.dataset.nosuchkey”"); }); // This test is skipped because it doesn’t pass, and looks like it would be // complicated to fix. it.skip("should reject a string value when referencing a data binding", function() { expectFailure(settingPlus({ show_if: { "data.dataset.key": "foo" } }, { data: [binding] })); }); it("should reject a reference to a data binding that has no key", function() { const binding_undef_key = { name: "No-key binding", dataset: "dataset", key: undefined, type: "column", column: "Foo::A" }; expectFailure(settingPlus({ show_if: "data.dataset" }, { data: [binding_undef_key] }), "template.yml: “show_if” or “hide_if” property specifies invalid data binding or column type “data.dataset”"); }); it("should accept settings whose names start with /data./ [kiln/flourish-sdk#45]", function() { const data_foo = { name: "Data foo", property: "data_foo", type: "string" }; expectSuccess(settingPlus({ show_if: { "data_foo": "xxx" } }, undefined, [data_foo])); }); }); describe("import", function() { describe("when the module to import exists", function() { let imported_module_directory, imported_settings_filename; before(function() { imported_module_directory = path.join(temp_directory, "node_modules/@flourish/layout/"); imported_settings_filename = path.join(imported_module_directory, "settings.yml"); fs.mkdirSync(imported_module_directory, { recursive: true }); fs.closeSync(fs.openSync(imported_settings_filename, "w")); sinon.stub(nodeResolve, "sync").returns(imported_settings_filename); }); after(function() { nodeResolve.sync.restore(); fs.unlinkSync(imported_settings_filename); }); it("should permit the import of a component", function() { expectSuccess(metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout" }] })); }); it("should allow for a show_if condition for an imported component", function() { expectSuccess(metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", show_if: true }] })); }); it("should allow for a hide_if condition for an imported component", function() { expectSuccess(metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", hide_if: true }] })); }); it("should not allow for a name property (for example) for an imported component", function() { expectFailure( metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", name: "Flourish" }] }), "template.yml: Unexpected property 'name' in import", ); }); it("should allow for an overrides array for an imported component", function() { expectSuccess(metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [] }] })); }); it("should throw if overrides is a string", function() { expectFailure( metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: "Hello Mark!" }] }), "template.yml Setting import overrides must be an array", ); }); it("should throw if overrides is an object", function() { expectFailure( metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: {} }] }), "template.yml Setting import overrides must be an array", ); }); it("should throw if an override is missing the 'property' or 'tag' property", function() { expectFailure( metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [{}] }] }), `template.yml Setting import overrides must each specify overridden “property” or “tag”`, ); }); it("should throw if an override has both 'property' and 'tag' property", function () { expectFailure( metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [{ property: "bg_color", tag: "categorical" }] }] }), `template.yml Setting import overrides cannot contain both “property” and “tag” property`, ); }); it("should allow for an override to have 'tag' in place of 'property'", function () { expectSuccess(metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [{ tag: "categorical" }] }] })); }); it("should allow for an override without a 'method' property", function() { expectSuccess(metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [{ property: "bg_color" }] }] })); }); it("should allow for an override with a 'method' property equal to 'replace'", function() { expectSuccess(metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [{ property: "bg_color", method: "replace" }] }] })); }); it("should allow for an override with a 'method' property equal to 'extend'", function() { expectSuccess(metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [{ property: "bg_color", method: "extend" }] }] })); }); it("should throw if an override has a 'method' property of (eg) 'delete'", function() { expectFailure( metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [{ property: "bg_color", method: "delete" }] }] }), `template.yml Setting import override “method” method must be either “replace” or “extend”`, ); }); }); describe("when the module to import doesn't exist", function() { it("should error when trying to import the component", function() { const expected_error = `Cannot find module '@flourish/layout/settings.yml' from '${temp_directory}'`; expectFailure( metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout" }] }), expected_error, ); }); }); }); describe("new_section", function() { it("should accept true", function() { expectSuccess(settingPlus({ new_section: true })); }); it("should accept false", function() { expectSuccess(settingPlus({ new_section: false })); }); it("should accept a string", function() { expectSuccess(settingPlus({ new_section: "section title" })); }); it("should reject null", function() { expectFailure(settingPlus({ new_section: null }), "template.yml setting “foo” new_section property must be a boolean or string, not null"); }); it("should reject numbers", function() { expectFailure(settingPlus({ new_section: 23 }), "template.yml setting “foo” new_section property must be a boolean or string, not number"); }); it("should reject arrays", function() { expectFailure(settingPlus({ new_section: [] }), "template.yml setting “foo” new_section property must be a boolean or string, not array"); }); it("should reject objects", function() { expectFailure(settingPlus({ new_section: {} }), "template.yml setting “foo” new_section property must be a boolean or string, not object"); }); }); describe("cacheable_for_standalone_downloads", function() { it("should accept true for url type", function() { expectSuccess(settingPlus({ type: "url", cacheable_for_standalone_downloads: true })); }); it("should accept false for url type", function() { expectSuccess(settingPlus({ type: "url", cacheable_for_standalone_downloads: false })); }); it("should reject it for string type", function() { expectFailure(settingPlus({ type: "string", cacheable_for_standalone_downloads: true }), "template.yml setting “foo” has “cacheable_for_standalone_downloads” property, but is not of type “url”"); }); it("should reject it for number type", function() { expectFailure(settingPlus({ type: "number", cacheable_for_standalone_downloads: true }), "template.yml setting “foo” has “cacheable_for_standalone_downloads” property, but is not of type “url”"); }); it("should reject it for boolean type", function() { expectFailure(settingPlus({ type: "boolean", cacheable_for_standalone_downloads: true }), "template.yml setting “foo” has “cacheable_for_standalone_downloads” property, but is not of type “url”"); }); it("should reject null", function() { expectFailure(settingPlus({ type: "url", cacheable_for_standalone_downloads: null }), "template.yml setting “foo” has an invalid value for “cacheable_for_standalone_downloads”: should be true or false"); }); it("should reject undefined", function() { expectFailure(settingPlus({ type: "url", cacheable_for_standalone_downloads: undefined }), "template.yml setting “foo” has an invalid value for “cacheable_for_standalone_downloads”: should be true or false"); }); it("should reject strings", function() { expectFailure(settingPlus(