From c735e786c9f49deb38c74b43e1dbedfaa5bc1dc1 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Mon, 15 Jul 2019 09:53:28 +0200 Subject: [PATCH 01/27] GPII-4022: Add JSDocs for new kettle validation functions. --- src/js/server/kettleValidation.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/js/server/kettleValidation.js b/src/js/server/kettleValidation.js index d5113ef..725fd2c 100644 --- a/src/js/server/kettleValidation.js +++ b/src/js/server/kettleValidation.js @@ -5,6 +5,17 @@ var gpii = fluid.registerNamespace("gpii"); fluid.registerNamespace("gpii.schema.kettle.validator"); +/** + * + * Validate a request payload according to a GSS Schema. Fulfills the contract for a `kettle.middleware` `handle` + * invoker. + * + * @param {Object} kettleValidator - A `gpii.schema.kettle.validator` instance that has a schema and rules about which part of the payload should be validated. + * @param {Object} globalValidator - The global `gpii.schema.validator` instance. + * @param {Object} requestHandler - The `kettle.request.http` grade that is fielding the actual request. + * @return {Promise} - A `fluid.promise` that is rejected with a validation error if the payload is invalid or resolved if the payload is valid. + * + */ gpii.schema.kettle.validator.validateRequest = function (kettleValidator, globalValidator, requestHandler) { var validationPromise = fluid.promise(); From bd2275b13d1889a5f49f76c82d8b502cc124056a Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Mon, 15 Jul 2019 14:48:24 +0200 Subject: [PATCH 02/27] GPII-4022: Cleaned up docs and "base" grades following kettle refactor. --- docs/schemaValidationMiddleware.md | 150 +++++++++++++++----- src/js/server/kettleValidation.js | 3 - src/js/server/schemaValidationMiddleware.js | 44 +++--- 3 files changed, 128 insertions(+), 69 deletions(-) diff --git a/docs/schemaValidationMiddleware.md b/docs/schemaValidationMiddleware.md index 4f0150d..a309c50 100644 --- a/docs/schemaValidationMiddleware.md +++ b/docs/schemaValidationMiddleware.md @@ -20,12 +20,13 @@ See below for usage examples for both kettle and gpii-express. The [`errorBinder`](errorBinder.md) component included with this package is designed to associate the validation error messages produced by the validator with on-screen elements. See that component's documentation for details. -## Components +## Express Components -### `gpii.schema.validationMiddleware.base` +### `gpii.schema.validationMiddleware` -The base grade for both kettle and gpii-express validation middleware. Validates information available in the request -object. The incoming request is first transformed using `fluid.model.transformWithRules` +The base grade for validation middleware used with gpii-express. Supports all the options above, plus the options for +[`gpii.express.middleware`](https://github.com/GPII/gpii-express/blob/master/docs/middleware.md#gpiiexpressmiddleware). +The incoming request is first transformed using `fluid.model.transformWithRules` and`options.rules.requestContentToValidate`. The results are validated against `options.schemaKey`. The default options validate the request body, as expected with a `POST` or `PUT` request. See the mix-in grades below @@ -43,11 +44,11 @@ The following component configuration options are supported: | Option | Type | Description | | -------------------------------- | -------- | ----------- | +| `errorTemplate` | `Object` | If there are validation errors, this object will be merged with the raw error to set the kettle-specific options like `message` and `statusCode`. | | `inputSchema` | `Object` | The [GSS](gss.md) schema to use in validating incoming request data. | | `rules.requestContentToValidate` | `Object` | The [rules to use in transforming](http://docs.fluidproject.org/infusion/development/ModelTransformationAPI.html#fluid-model-transformwithrules-source-rules-options-) the incoming data before validation (see below for more details). | -The default `rules.requestContentToValidate` in this grade are intended for use with `PUT` or `POST` body data. This -can be represented as follows: +The default `rules.requestContentToValidate` in the express middleware grade can be represented as follows: ```snippet requestContentToValidate: { @@ -55,14 +56,8 @@ requestContentToValidate: { } ``` -These rules extract POST and PUT payloads from the request body created by the [Express body parser -middleware](https://github.com/expressjs/body-parser). See `gpii.schema.validationMiddleware.handlesGetMethod` below -for an example of working with query data. - -### `gpii.schema.validationMiddleware` - -The base grade for validation middleware used with gpii-express. Supports all the options above, plus the options for -[`gpii.express.middleware`](https://github.com/GPII/gpii-express/blob/master/docs/middleware.md#gpiiexpressmiddleware). +This transformation exposes only the body of the request as a top-level object to be validated. For another example, +see `gpii.schema.validationMiddleware.handlesQueryData` below. #### Invokers @@ -86,26 +81,6 @@ function and let some other downstream piece of middleware continue the conversa This function is expected to be called by Express (or by an instance of `gpii.express`). -## `gpii.schema.kettle.middleware` - -The schema validation [kettle middleware](https://github.com/fluid-project/kettle/blob/master/docs/Middleware.md). Must -be used in combination with a grade that derives from `gpii.schema.kettle.request.http`. See "kettle example" below -for an example of using this grade. - -### Component Options - -In addition to the options supported by the base grade above, this grade supports the following options: - -| Option | Type | Description | -| ---------------- | -------- | ----------- | -| `errorTemplate` | `Object` | If there are validation errors, this object will be merged with the raw error to set the kettle-specific options like `message` and `statusCode`. | - -## `gpii.schema.kettle.request.http` - -The [request handler](https://github.com/fluid-project/kettle/blob/master/docs/RequestHandlersAndApps.md) portion of -the kettle schema validation middleware. Must be used in combination with a grade that derives from -`gpii.schema.kettle.middleware`. See "kettle example" below for an example of using this grade. - ### `gpii.schema.validationMiddleware.handlesQueryData` A mix-in grade that configures a grade that derives from `gpii.schema.validationMiddleware.base` (so, either the @@ -117,7 +92,7 @@ requestContentToValidate: { } ``` -## gpii-express example +### gpii-express example The `gpii.schema.validationMiddleware` grade is intended to be used with a `gpii.express` or `gpii.express.router` instance. The `gpii.schema.validationMiddleware.requestAware.router` wrapper is provided as a convenient starting @@ -158,7 +133,66 @@ to the schema `valid.json`, which can be found in `%my-package/src/schemas`. If schema, the handler defined above would output a canned "success" message. If the payload is invalid, the underlying `gpii.express.middleware` instance steps in and responds with a failure message. -## kettle example +## Kettle Components + +### `gpii.schema.kettle.validator` + +An extension of the `kettle.middleware` grade that is intended to be hosted as a child of your +[`kettle.app`]((https://github.com/fluid-project/kettle/blob/master/docs/RequestHandlersAndApps.md)) grade, and to be +referenced as `requestMiddleware` from one or more of your `kettle.request.http` instances. Each validator validates +a single type of payload. See below for a usage example. + +#### Component Options + +The following component configuration options are supported: + +| Option | Type | Description | +| -------------------------------- | -------- | ----------- | +| `errorTemplate` | `Object` | If there are validation errors, this object will be merged with the raw error to set the kettle-specific options like `message` and `statusCode`. | +| `requestSchema` | `Object` | The [GSS](gss.md) schema to use in validating incoming request data. | +| `rules.requestContentToValidate` | `Object` | The [rules to use in transforming](http://docs.fluidproject.org/infusion/development/ModelTransformationAPI.html#fluid-model-transformwithrules-source-rules-options-) the incoming data before validation (see below for more details). | + +The default `rules.requestContentToValidate` in the express middleware grade can be represented as follows: + +```snippet +requestContentToValidate: { + "body": "body", + "params": "params", + "query": "query" +} +``` + +This transformation strips complex internal material from the underlying request and exposes only the parameters ( +`params`), query string data (`query`) and request body (`body`). There are convenience grades provided for payloads +where only the query, parameters, or body are validated. See below. + +#### Invokers + +##### `{gpii.schema.kettle.validator}.handle(requestComponent)` + +* `requestComponent`: The `kettle.request.http` instance fielding the actual request. +* Returns: A [`fluid.promise`](https://docs.fluidproject.org/infusion/development/PromisesAPI.html) that will be + resolved if the payload is valid, or rejected with validation errors if the payload is invalid. + +This invoker satisfies the basic contract for a `kettle.middleware` grade. It validates a payload and returns a promise +that is resolved if processing should continue or rejected if an invalid payload is detected. + +### `gpii.schema.kettle.validator.body` + +A convenience validator grade that exposes the request body as a top-level object, so that you can write simpler +schemas to validate your payloads. + +### `gpii.schema.kettle.validator.params` + +A convenience validator grade that exposes only the URL parameters as a top-level object, so that you can write simpler +schemas to validate your payloads. + +### `gpii.schema.kettle.validator.query` + +A convenience validator grade that exposes only the query string parameters as a top-level object, so that you can write +simpler schemas to validate your payloads. + +### Kettle example ```javascript var fluid = require("infusion"); @@ -174,9 +208,8 @@ my.kettle.handler.reportSuccess = function (request) { request.events.onSuccess.fire({ message: "Payload accepted." }); }; -// Looking for body content and validate that against our schema. -fluid.defaults("my.kettle.handler", { - gradeNames: ["gpii.schema.kettle.request.http"], +fluid.defaults("my.kettle.validator", { + gradeNames: ["gpii.schema.kettle.validator.body"], inputSchema: { type: "object", properties: { @@ -187,6 +220,16 @@ fluid.defaults("my.kettle.handler", { enumLabels: ["Good Choice"] } } + } +}); + +// Looking for body content and validate that against our schema. +fluid.defaults("my.kettle.handler", { + gradeNames: ["kettle.request.http"], + requestMiddleware: { + validate: { + middleware: "{my.kettle.app}.myValidator" + } }, invokers: { handleRequest: { @@ -197,6 +240,11 @@ fluid.defaults("my.kettle.handler", { fluid.defaults("my.kettle.app", { gradeNames: ["kettle.app"], + components: { + myValidator: { + type: "my.kettle.validator" + } + }, requestHandlers: { gatedBody: { type: "my.kettle.handler", @@ -213,3 +261,27 @@ If you were to run the above example, you would have a kettle app with a `/gated payload that does not contain a `hasBodyContent` element that is specifically set to the string `good`. Any payload that contains that element with the correct value would be passed to the underlying handler stub, and result in a "payload accepted" message. + +Note that we use the `gpii.schema.kettle.validator.body` grade to keep our schema simple. If we were to use the base +`gpii.schema.kettle.validator` grade, our schema might look like: + +```json5 +{ + type: "object", + properties: { + body: { + properties: { + hasBodyContent: { + type: "string", + required: true, + enum: ["good"], + enumLabels: ["Good Choice"] + } + } + } + } +} +``` + +Each of the convenience grades above allow you to avoid one layer of object-and-property nesting when you are only +dealing with one type of data. diff --git a/src/js/server/kettleValidation.js b/src/js/server/kettleValidation.js index 725fd2c..97a944a 100644 --- a/src/js/server/kettleValidation.js +++ b/src/js/server/kettleValidation.js @@ -42,9 +42,6 @@ gpii.schema.kettle.validator.validateRequest = function (kettleValidator, global // A kettle.middleware grade that can be used in the requestMiddleware stack, as in: // https://github.com/fluid-project/kettle/blob/670396acbf4be31be009b2b2dee48373134ea94d/tests/shared/SessionTestDefs.js#L64 -// TODO: refactor tests to use single per-payload validation. - - fluid.defaults("gpii.schema.kettle.validator", { gradeNames: ["kettle.middleware", "fluid.modelComponent"], schemaHash: "@expand:gpii.schema.hashSchema({that}.options.requestSchema)", diff --git a/src/js/server/schemaValidationMiddleware.js b/src/js/server/schemaValidationMiddleware.js index c61342d..ba1df3b 100644 --- a/src/js/server/schemaValidationMiddleware.js +++ b/src/js/server/schemaValidationMiddleware.js @@ -59,14 +59,15 @@ gpii.schema.validationMiddleware.rejectOrForward = function (validatorComponent ); }; - /* - The base middleware used with both gpii-express and kettle. Cannot be used on its own. + The `gpii.express.middleware` that fields invalid responses itself and passes valid ones through to the `next` + Express router or middleware function. Must be combined with either the `requestAware` or `contentAware` grades + to function properly. See the grades below for an example. */ -fluid.defaults("gpii.schema.validationMiddleware.base", { - gradeNames: ["fluid.modelComponent"], +fluid.defaults("gpii.schema.validationMiddleware", { + gradeNames: ["fluid.modelComponent", "gpii.schema.component", "gpii.express.middleware"], namespace: "validationMiddleware", // A namespace that can be used to order other middleware relative to this component. inputSchema: { "$schema": "gss-v7-full#" @@ -87,29 +88,6 @@ fluid.defaults("gpii.schema.validationMiddleware.base", { "": "body" } }, - invokers: { - middleware: { - funcName: "gpii.schema.validationMiddleware.rejectOrForward", - args: ["{gpii.schema.validator}", "{that}", "{that}.options.inputSchema", "{arguments}.0", "{arguments}.1", "{arguments}.2"] // schema, request, response, next - } - }, - listeners: { - "onCreate.cacheSchema": { - func: "{gpii.schema.validator}.cacheSchema", - args: ["{that}.options.inputSchema"] - } - } -}); - -/* - - The `gpii.express.middleware` that fields invalid responses itself and passes valid ones through to the `next` - Express router or middleware function. Must be combined with either the `requestAware` or `contentAware` grades - to function properly. See the grades below for an example. - - */ -fluid.defaults("gpii.schema.validationMiddleware", { - gradeNames: ["gpii.schema.component", "gpii.express.middleware", "gpii.schema.validationMiddleware.base"], schema: { properties: { inputSchema: { @@ -128,6 +106,18 @@ fluid.defaults("gpii.schema.validationMiddleware", { } } } + }, + invokers: { + middleware: { + funcName: "gpii.schema.validationMiddleware.rejectOrForward", + args: ["{gpii.schema.validator}", "{that}", "{that}.options.inputSchema", "{arguments}.0", "{arguments}.1", "{arguments}.2"] // schema, request, response, next + } + }, + listeners: { + "onCreate.cacheSchema": { + func: "{gpii.schema.validator}.cacheSchema", + args: ["{that}.options.inputSchema"] + } } }); From c23df3ad4d6910c9210d99bf0bcaa7ce76145296 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Thu, 18 Jul 2019 11:17:11 +0200 Subject: [PATCH 03/27] GPII-4029: Update `clearCache` to consistently clear the cache. --- src/js/common/validator.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/js/common/validator.js b/src/js/common/validator.js index 36be831..b07faf7 100644 --- a/src/js/common/validator.js +++ b/src/js/common/validator.js @@ -212,6 +212,18 @@ var fluid = fluid || require("infusion"); delete that.validatorsByHash[schemaHash]; }; + /** + * + * Clear all cached validators. + * + * @param {Object} that - The validator component itself. + * + */ + gpii.schema.validator.clearCache = function (that) { + delete that.validatorsByHash; + that.validatorsByHash = {}; + }; + /** * @param {GssSchema} gssSchema - A GSS schema definition. * @param {Object} ajvOptions - Optional arguments to pass to the underlying AJV validator. @@ -653,8 +665,8 @@ var fluid = fluid || require("infusion"); args: ["{that}", "{arguments}.0", "{arguments}.1"] // gssSchema, schemaHash }, clearCache: { - funcName: "fluid.set", - args: ["{that}", "validatorsByHash", {}] // model, path, newValue + funcName: "gpii.schema.validator.clearCache", + args: ["{that}"] } } }); From 444b20c38bf7c4009adea4b459455f350cff15c3 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Thu, 18 Jul 2019 13:13:41 +0200 Subject: [PATCH 04/27] GPII-4022: Refactored kettle grades to refer to validators by grade rather than as a named child component of a kettle.app grade. --- docs/schemaValidationMiddleware.md | 2 +- tests/js/node/lib/kettle-test-fixtures.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/schemaValidationMiddleware.md b/docs/schemaValidationMiddleware.md index a309c50..6bfc5e1 100644 --- a/docs/schemaValidationMiddleware.md +++ b/docs/schemaValidationMiddleware.md @@ -228,7 +228,7 @@ fluid.defaults("my.kettle.handler", { gradeNames: ["kettle.request.http"], requestMiddleware: { validate: { - middleware: "{my.kettle.app}.myValidator" + middleware: "{my.kettle.validator}" } }, invokers: { diff --git a/tests/js/node/lib/kettle-test-fixtures.js b/tests/js/node/lib/kettle-test-fixtures.js index 0fa677f..982ddc1 100644 --- a/tests/js/node/lib/kettle-test-fixtures.js +++ b/tests/js/node/lib/kettle-test-fixtures.js @@ -47,7 +47,7 @@ fluid.defaults("gpii.test.schema.kettle.handlers.gatedBody", { gradeNames: ["gpii.test.schema.kettle.handlers.base"], requestMiddleware: { validate: { - middleware: "{gpii.test.schema.kettle.app}.bodyValidator" + middleware: "{gpii.test.schema.kettle.bodyValidator}" } } }); @@ -72,7 +72,7 @@ fluid.defaults("gpii.test.schema.kettle.handlers.gatedParams", { gradeNames: ["gpii.test.schema.kettle.handlers.base"], requestMiddleware: { validate: { - middleware: "{gpii.test.schema.kettle.app}.paramsValidator" + middleware: "{gpii.test.schema.kettle.paramsValidator}" } } }); @@ -98,7 +98,7 @@ fluid.defaults("gpii.test.schema.kettle.handlers.gatedQuery", { gradeNames: ["gpii.test.schema.kettle.handlers.base"], requestMiddleware: { validate: { - middleware: "{gpii.test.schema.kettle.app}.queryValidator" + middleware: "{gpii.test.schema.kettle.queryValidator}" } } }); @@ -151,7 +151,7 @@ fluid.defaults("gpii.test.schema.kettle.handlers.gatedCombined", { gradeNames: ["gpii.test.schema.kettle.handlers.base"], requestMiddleware: { validate: { - middleware: "{gpii.test.schema.kettle.app}.combinedValidator" + middleware: "{gpii.test.schema.kettle.combinedValidator}" } } }); From ae785224fe258d8d6679ef66f3596b2ff6f63954 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Thu, 29 Aug 2019 11:29:13 +0200 Subject: [PATCH 05/27] GPII-4098: Add a "bad request" status code to the express validation middleware. --- src/js/server/schemaValidationMiddleware.js | 2 ++ tests/js/node/lib/harness.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/js/server/schemaValidationMiddleware.js b/src/js/server/schemaValidationMiddleware.js index ba1df3b..48c52f4 100644 --- a/src/js/server/schemaValidationMiddleware.js +++ b/src/js/server/schemaValidationMiddleware.js @@ -52,6 +52,7 @@ gpii.schema.validationMiddleware.rejectOrForward = function (validatorComponent var localisedErrors = gpii.schema.validator.localiseErrors(validationResults.errors, toValidate, schemaMiddlewareComponent.model.messages, schemaMiddlewareComponent.options.localisationTransform); var localisedPayload = fluid.copy(validationResults); localisedPayload.errors = localisedErrors; + localisedPayload.statusCode = schemaMiddlewareComponent.options.invalidStatusCode; next(localisedPayload); } }, @@ -69,6 +70,7 @@ gpii.schema.validationMiddleware.rejectOrForward = function (validatorComponent fluid.defaults("gpii.schema.validationMiddleware", { gradeNames: ["fluid.modelComponent", "gpii.schema.component", "gpii.express.middleware"], namespace: "validationMiddleware", // A namespace that can be used to order other middleware relative to this component. + invalidStatusCode: 400, inputSchema: { "$schema": "gss-v7-full#" }, diff --git a/tests/js/node/lib/harness.js b/tests/js/node/lib/harness.js index dff2a04..f283c7c 100644 --- a/tests/js/node/lib/harness.js +++ b/tests/js/node/lib/harness.js @@ -99,12 +99,12 @@ fluid.defaults("gpii.test.schema.harness", { templateKey: "partials/validation-error-summary" } }, - // This should never be reached + // This is hit by validation errors that are not otherwise handled (for example, by rendering the error). defaultErrorMiddleware: { type: "gpii.express.middleware.error", options: { priority: "last", - defaultStatusCode: 400 + defaultStatusCode: 500 } } } From fc01d7be5fc851161cfc5f6308eeb17413210ab7 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Thu, 29 Aug 2019 11:29:59 +0200 Subject: [PATCH 06/27] GPII-4022: Fixed lingering documentation example that was still using 1.0-era conventions. --- docs/schemaValidationMiddleware.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/schemaValidationMiddleware.md b/docs/schemaValidationMiddleware.md index 6bfc5e1..58797c7 100644 --- a/docs/schemaValidationMiddleware.md +++ b/docs/schemaValidationMiddleware.md @@ -119,19 +119,23 @@ fluid.defaults("gpii.schema.tests.handler", { gpii.express({ gradeNames: ["gpii.schema.validationMiddleware.requestAware.router"], handlerGrades: ["gpii.schema.tests.handler"], - schemaKey: "valid.json", - schemaDirs: ["%my-package/src/schemas"], - responseSchemaKey: "message.json", - responseSchemaUrl: "http://my.site/schemas/", + requestSchema: { + properties: { + key: { + type: "string", + required: true + } + } + }, path: "/gatekeeper", port: 3000 }); ``` If you were to launch this example, you would have a REST endpoint `/gatekeeper` that compares all POST request payloads -to the schema `valid.json`, which can be found in `%my-package/src/schemas`. If a payload is valid according to the -schema, the handler defined above would output a canned "success" message. If the payload is invalid, the underlying -`gpii.express.middleware` instance steps in and responds with a failure message. +to the requestSchema. If a payload is valid according to the schema, the handler defined above would output a canned +"success" message. If the payload is invalid, the underlying `gpii.express.middleware` instance steps in and responds +with a failure message. ## Kettle Components From b94c698ee777adeeea8b56d1ae391f77200dfa40 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Thu, 29 Aug 2019 12:03:29 +0200 Subject: [PATCH 07/27] GPII-4022: Fixed additional typo in express example. --- docs/schemaValidationMiddleware.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/schemaValidationMiddleware.md b/docs/schemaValidationMiddleware.md index 58797c7..dae2a50 100644 --- a/docs/schemaValidationMiddleware.md +++ b/docs/schemaValidationMiddleware.md @@ -117,9 +117,9 @@ fluid.defaults("gpii.schema.tests.handler", { }); gpii.express({ - gradeNames: ["gpii.schema.validationMiddleware.requestAware.router"], - handlerGrades: ["gpii.schema.tests.handler"], - requestSchema: { + gradeNames: ["gpii.schema.validationMiddleware.requestAware.router"], + handlerGrades: ["gpii.schema.tests.handler"], + inputSchema: { properties: { key: { type: "string", @@ -127,8 +127,8 @@ gpii.express({ } } }, - path: "/gatekeeper", - port: 3000 + path: "/gatekeeper", + port: 3000 }); ``` @@ -153,7 +153,7 @@ The following component configuration options are supported: | Option | Type | Description | | -------------------------------- | -------- | ----------- | | `errorTemplate` | `Object` | If there are validation errors, this object will be merged with the raw error to set the kettle-specific options like `message` and `statusCode`. | -| `requestSchema` | `Object` | The [GSS](gss.md) schema to use in validating incoming request data. | +| `inputSchema` | `Object` | The [GSS](gss.md) schema to use in validating incoming request data. | | `rules.requestContentToValidate` | `Object` | The [rules to use in transforming](http://docs.fluidproject.org/infusion/development/ModelTransformationAPI.html#fluid-model-transformwithrules-source-rules-options-) the incoming data before validation (see below for more details). | The default `rules.requestContentToValidate` in the express middleware grade can be represented as follows: From 5096ff65689d63cc8af21557397db72821163c7e Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Tue, 17 Sep 2019 13:52:25 +0200 Subject: [PATCH 08/27] GPII-3929: Added "schema holder" base grade and tests. --- src/js/common/schemaHolder.js | 107 +++++++++++ tests/js/common/schema-holder-tests.js | 247 +++++++++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 src/js/common/schemaHolder.js create mode 100644 tests/js/common/schema-holder-tests.js diff --git a/src/js/common/schemaHolder.js b/src/js/common/schemaHolder.js new file mode 100644 index 0000000..0104488 --- /dev/null +++ b/src/js/common/schemaHolder.js @@ -0,0 +1,107 @@ +/* + + This file defines the core concept of a "schema holder", a grades that holds a schema that can be used to validate + arbitrary user payloads. It also provides the core "user" schema extended by the specific schemas used by + the REST API endpoints in this package. + +*/ +/* globals require */ +(function (fluid) { + "use strict"; + if (!fluid) { + fluid = require("infusion"); + fluid.require("%gpii-json-schema"); + } + + var gpii = fluid.registerNamespace("gpii"); + + fluid.registerNamespace("gpii.schema.schemaHolder"); + + gpii.schema.schemaHolder.generateIfNeeded = function (that) { + return that.generatedSchema ? fluid.toPromise(that.generatedSchema) : that.generateSchema(); + }; + + // We cannot simply use fluid.set here because it does not return the set value, and would strip the value from the + // promise chain. + gpii.schema.schemaHolder.cacheSchema = function (that, schema) { + that.generatedSchema = schema; + return schema; + }; + + gpii.schema.schemaHolder.incorporateSubcomponentSchemas = function (that, baseSchema) { + var subComponents = fluid.queryIoCSelector(that, "gpii.schema.schemaHolder", true); + fluid.each(subComponents, function (subComponent) { + // getSchema is asynchronous, so we have to use a "promise chain" to handle this. + that.events.incorporateSubcomponentSchemas.addListener(function (baseSchema) { + var singleComponentSchemaPromise = fluid.promise(); + subComponent.getSchema().then( + function (subComponentSchema) { + try { + var mergedSchema = fluid.model.transformWithRules({ baseSchema: baseSchema, toMerge: subComponentSchema }, that.options.rules.mergeSubcomponentSchema); + singleComponentSchemaPromise.resolve(mergedSchema); + } + catch (error) { + singleComponentSchemaPromise.reject(error); + } + }, + singleComponentSchemaPromise.reject + ); + return singleComponentSchemaPromise; + }); + }); + return fluid.promise.fireTransformEvent(that.events.incorporateSubcomponentSchemas, baseSchema); + }; + + fluid.defaults("gpii.schema.schemaHolder", { + gradeNames: ["fluid.component"], + mergePolicy: { + "rules.mergeSubcomponentSchema": "nomerge" + }, + rules: { + // By default, simply ignore any child schemas. + mergeSubcomponentSchema: { + "": "baseSchema" + } + }, + events: { + getSchema: null, + generateSchema: null, + incorporateSubcomponentSchemas: null + }, + members: { + generatedSchema: false, + subComponentSchemaPromises: [] + }, + schema: { + $schema: "gss-v7-full#", + additionalProperties: true + }, + invokers: { + getSchema: { + funcName: "gpii.schema.schemaHolder.generateIfNeeded", + args: ["{that}"] + }, + generateSchema: { + funcName: "fluid.promise.fireTransformEvent", + args: ["{that}.events.generateSchema"] + } + }, + listeners: { + "generateSchema.getOptions": { + priority: "first", + funcName: "fluid.identity", + args: ["{that}.options.schema"] + }, + "generateSchema.incorporateComponentSchemas": { + priority: "after:getOptions", + funcName: "gpii.schema.schemaHolder.incorporateSubcomponentSchemas", + args: ["{that}", "{arguments}.0"] + }, + "generateSchema.cacheSchema": { + priority: "last", + funcName: "gpii.schema.schemaHolder.cacheSchema", + args: ["{that}", "{arguments}.0"] + } + } + }); +})(fluid); diff --git a/tests/js/common/schema-holder-tests.js b/tests/js/common/schema-holder-tests.js new file mode 100644 index 0000000..711b559 --- /dev/null +++ b/tests/js/common/schema-holder-tests.js @@ -0,0 +1,247 @@ +/* eslint-env browser */ +/* globals require */ +var fluid = fluid || {}; +var jqUnit = jqUnit || {}; + +(function (fluid, jqUnit) { + "use strict"; + if (!fluid.identity) { + fluid = require("infusion"); + jqUnit = require("node-jqunit"); + require("../../../src/js/common/schemaHolder"); + } + var gpii = fluid.registerNamespace("gpii"); + + // A schema holder with no children. + fluid.defaults("gpii.tests.schema.schemaHolder", { + gradeNames: ["gpii.schema.schemaHolder"], + schema: { + type: "object", + properties: { + childProperty: {type: "string"} + } + } + }); + + // This transform is a crude demonstration of what we might do with the LSR, where we need to store multiple + // settings in a generated supportedSettings block. + fluid.defaults("gpii.tests.schema.schemaHolder.transforms.mergeChild", { + gradeNames: ["fluid.transformFunction"] + }); + + fluid.registerNamespace("gpii.tests.schema.schemaHolder.transforms"); + gpii.tests.schema.schemaHolder.transforms.mergeChild = function (transformSpec, transformer) { + if (!transformSpec.toMerge) { + fluid.fail("mergeChild transform requires a `toMerge` inputPath.", transformSpec); + } + var toMerge = transformer.expand(transformSpec.toMerge); + // TODO: pass both components rather than just their schemas, so that we can use some aspect of the component + // rather than the "title" property of the schema. + var outputPath = fluid.model.composePaths(transformer.outputPrefix, toMerge.title); + transformer.applier.change(outputPath, toMerge); + }; + + // A schema holder with multiple children. + fluid.defaults("gpii.tests.schema.schemaHolder.parent", { + gradeNames: ["gpii.schema.schemaHolder"], + schema: { + type: "object", + properties: { + parentProperty: {type: "string"} + } + }, + rules: { + mergeSubcomponentSchema: { + "": "baseSchema", + properties: { + "": "baseSchema.properties", + transform: { + type: "gpii.tests.schema.schemaHolder.transforms.mergeChild", + toMerge: "toMerge" + } + } + } + }, + components: { + oneChild: { + type: "gpii.tests.schema.schemaHolder", + options: { + schema: { + title: "oldest child" + } + } + }, + anotherChild: { + type: "gpii.tests.schema.schemaHolder", + options: { + schema: { + title: "youngest child" + } + } + } + } + }); + + // A schema holder with a single child and multiple grandchildren. + fluid.defaults("gpii.tests.schema.schemaHolder.grandparent", { + gradeNames: ["gpii.schema.schemaHolder"], + schema: { + type: "object", + properties: { + grandParentProperty: {type: "string"} + } + }, + rules: { + mergeSubcomponentSchema: { + "": "baseSchema", + // We use a simpler approach here as we only expect one child component. + properties: { + child: "toMerge" + } + } + }, + components: { + child: { + type: "gpii.tests.schema.schemaHolder.parent" + } + } + }); + + gpii.tests.schema.schemaHolder.failOnError = function (error) { + jqUnit.start(); + jqUnit.fail(error); + }; + + gpii.tests.schema.schemaHolder.generateCheckOnResolveFn = function (message, expected) { + return function (schema) { + jqUnit.start(); + jqUnit.assertDeepEq(message, expected, schema); + }; + }; + + + jqUnit.module("Schema holder tests."); + + jqUnit.asyncTest("Testing a schema holder with no children.", function () { + var expected = { + "$schema": "gss-v7-full#", + additionalProperties: true, + type: "object", + properties: { + childProperty: {type: "string"} + } + }; + var component = gpii.tests.schema.schemaHolder(); + component.getSchema().then( + gpii.tests.schema.schemaHolder.generateCheckOnResolveFn("The schema should be as expected", expected), + gpii.tests.schema.schemaHolder.failOnError + ); + }); + + jqUnit.asyncTest("Testing schema caching.", function () { + var expected = { + "$schema": "gss-v7-full#", + additionalProperties: true, + type: "object", + properties: { + childProperty: {type: "string"} + } + }; + var component = gpii.tests.schema.schemaHolder(); + component.getSchema().then( + function () { + component.getSchema().then( + gpii.tests.schema.schemaHolder.generateCheckOnResolveFn("The cached schema should be as expected", expected), + gpii.tests.schema.schemaHolder.failOnError + ); + }, + gpii.tests.schema.schemaHolder.failOnError + ); + }); + + jqUnit.asyncTest("Testing a component with multiple child schema holders.", function () { + var expected = { + "$schema": "gss-v7-full#", + additionalProperties: true, + type: "object", + properties: { + parentProperty: {type: "string"}, + "oldest child": { + "$schema": "gss-v7-full#", + additionalProperties: true, + title: "oldest child", + type: "object", + properties: { + childProperty: {type: "string"} + } + }, + "youngest child": { + "$schema": "gss-v7-full#", + title: "youngest child", + additionalProperties: true, + type: "object", + properties: { + childProperty: {type: "string"} + } + } + } + }; + var component = gpii.tests.schema.schemaHolder.parent(); + component.getSchema().then( + gpii.tests.schema.schemaHolder.generateCheckOnResolveFn("The schema should have been merged with our children's schemas.", expected), + gpii.tests.schema.schemaHolder.failOnError + ); + }); + + jqUnit.asyncTest("Testing a component with multiple levels of child schema holders.", function () { + fluid.logObjectRenderChars = 10240; + var expected = { + "type": "object", + "properties": { + "grandParentProperty": { + "type": "string" + }, + "child": { + "type": "object", + "properties": { + "parentProperty": { + "type": "string" + }, + "oldest child": { + "title": "oldest child", + "type": "object", + "properties": { + "childProperty": { + "type": "string" + } + }, + "$schema": "gss-v7-full#", + "additionalProperties": true + }, + "youngest child": { + "title": "youngest child", + "type": "object", + "properties": { + "childProperty": { + "type": "string" + } + }, + "$schema": "gss-v7-full#", + "additionalProperties": true + } + }, + "$schema": "gss-v7-full#", + "additionalProperties": true + } + }, + "$schema": "gss-v7-full#", + "additionalProperties": true + }; + + var component = gpii.tests.schema.schemaHolder.grandparent(); + component.getSchema().then( + gpii.tests.schema.schemaHolder.generateCheckOnResolveFn("The schema should have been merged with our children's schemas.", expected), + gpii.tests.schema.schemaHolder.failOnError + ); + }); +})(fluid, jqUnit); From 083b60294796c2e15afd4efc15241e89e88af2c8 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Thu, 19 Sep 2019 10:25:15 +0200 Subject: [PATCH 09/27] GPII-4022: Removed unused `getSchema` event from schema holder draft. --- src/js/common/schemaHolder.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/js/common/schemaHolder.js b/src/js/common/schemaHolder.js index 0104488..14b8385 100644 --- a/src/js/common/schemaHolder.js +++ b/src/js/common/schemaHolder.js @@ -64,7 +64,6 @@ } }, events: { - getSchema: null, generateSchema: null, incorporateSubcomponentSchemas: null }, From e24732675c39f8580366e8eec4e5e36ea132585e Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Thu, 19 Sep 2019 10:32:50 +0200 Subject: [PATCH 10/27] GPII-4022: Clarify what type of component is expected to be in put to various functions. --- src/js/client/errorBinder.js | 2 +- src/js/common/schemaValidatedComponent.js | 2 +- src/js/common/schemaValidatedModelComponent.js | 4 ++-- src/js/common/validator.js | 9 ++++----- src/js/server/kettleValidation.js | 6 +++--- src/js/server/schemaValidationMiddleware.js | 4 ++-- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/js/client/errorBinder.js b/src/js/client/errorBinder.js index 5731d64..471ff42 100644 --- a/src/js/client/errorBinder.js +++ b/src/js/client/errorBinder.js @@ -89,7 +89,7 @@ * * A gatekeeper function that only allows form submission if there are no validation errors. * - * @param {Object} that - The clientSideValidation component itself. + * @param {gpii.schema.client.errorAwareForm} that - The clientSideValidation component itself. * @param {String} event - The jQuery Event (see http://api.jquery.com/Types/#Event) passed by the DOM element we're bound to. */ gpii.schema.client.errorAwareForm.submitForm = function (that, event) { diff --git a/src/js/common/schemaValidatedComponent.js b/src/js/common/schemaValidatedComponent.js index cb0ae57..68d0652 100644 --- a/src/js/common/schemaValidatedComponent.js +++ b/src/js/common/schemaValidatedComponent.js @@ -30,7 +30,7 @@ var fluid = fluid || require("infusion"); * We have our own validation code here because the global validator is not available in the context of a * potentia-ii workflow. * - * @param {Object} componentToValidate - The component to validate. + * @param {gpii.schema.component} componentToValidate - The component to validate. * */ gpii.schema.component.validateComponent = function (componentToValidate) { diff --git a/src/js/common/schemaValidatedModelComponent.js b/src/js/common/schemaValidatedModelComponent.js index d7a8501..063dca7 100644 --- a/src/js/common/schemaValidatedModelComponent.js +++ b/src/js/common/schemaValidatedModelComponent.js @@ -15,8 +15,8 @@ var fluid = fluid || require("infusion"); * * Validate the model against the associated schema (options.modelSchema). * - * @param {Object} globalValidator - The global validation component. - * @param {Object} modelValidationComponent - The component itself. + * @param {gpii.schema.validator} globalValidator - The global validation component. + * @param {gpii.schema.modelComponent} modelValidationComponent - The component itself. * */ gpii.schema.modelComponent.validateModel = function (globalValidator, modelValidationComponent) { diff --git a/src/js/common/validator.js b/src/js/common/validator.js index b07faf7..9f81bad 100644 --- a/src/js/common/validator.js +++ b/src/js/common/validator.js @@ -157,7 +157,7 @@ var fluid = fluid || require("infusion"); * * Validate material against a "GPII Schema System" schema using a precompiled AJV "validator". * - * @param {Object} that - The validator component. + * @param {gpii.schema.validator} that - The validator component. * @param {GssSchema} gssSchema - A GSS schema definition. * @param {Any} toValidate - The material to be validated. * @param {String} [schemaHash] - An optional precomputed hash of the schema. @@ -185,7 +185,7 @@ var fluid = fluid || require("infusion"); * * Add a single GSS schema to the cache. * - * @param {Object} that - The validator component itself. + * @param {gpii.schema.validator} that - The validator component itself. * @param {GssSchema} gssSchema - The original GSS schema. * @param {String} [schemaHash] - An optional precomputed hash of the schema. * @return {Object} - The compiled validator created from the GSS schema. @@ -202,7 +202,7 @@ var fluid = fluid || require("infusion"); * * Remove a single previously cached schema from the cache. * - * @param {Object} that - The validator component itself. + * @param {gpii.schema.validator} that - The validator component itself. * @param {GssSchema} gssSchema - The original GSS schema. * @param {String} [schemaHash] - An optional precomputed hash of the schema. * @@ -216,11 +216,10 @@ var fluid = fluid || require("infusion"); * * Clear all cached validators. * - * @param {Object} that - The validator component itself. + * @param {gpii.schema.validator} that - The validator component itself. * */ gpii.schema.validator.clearCache = function (that) { - delete that.validatorsByHash; that.validatorsByHash = {}; }; diff --git a/src/js/server/kettleValidation.js b/src/js/server/kettleValidation.js index 97a944a..a16e93c 100644 --- a/src/js/server/kettleValidation.js +++ b/src/js/server/kettleValidation.js @@ -10,9 +10,9 @@ fluid.registerNamespace("gpii.schema.kettle.validator"); * Validate a request payload according to a GSS Schema. Fulfills the contract for a `kettle.middleware` `handle` * invoker. * - * @param {Object} kettleValidator - A `gpii.schema.kettle.validator` instance that has a schema and rules about which part of the payload should be validated. - * @param {Object} globalValidator - The global `gpii.schema.validator` instance. - * @param {Object} requestHandler - The `kettle.request.http` grade that is fielding the actual request. + * @param {gpii.schema.kettle.validator} kettleValidator - A `gpii.schema.kettle.validator` instance that has a schema and rules about which part of the payload should be validated. + * @param {gpii.schema.validator} globalValidator - The global validator instance. + * @param {kettle.request.http} requestHandler - The component that is fielding the actual request. * @return {Promise} - A `fluid.promise` that is rejected with a validation error if the payload is invalid or resolved if the payload is valid. * */ diff --git a/src/js/server/schemaValidationMiddleware.js b/src/js/server/schemaValidationMiddleware.js index 48c52f4..6823f13 100644 --- a/src/js/server/schemaValidationMiddleware.js +++ b/src/js/server/schemaValidationMiddleware.js @@ -26,8 +26,8 @@ require("../common/schemaValidatedComponent"); * no arguments. If there are errors, the `next` callback is called with a localised/internationalised copy of the * validation errors. * - * @param {Object} validatorComponent - The middleware component itself. - * @param {Object} schemaMiddlewareComponent - The middleware component itself. + * @param {gpii.schema.validator} validatorComponent - The global validator component. + * @param {gpii.schema.validationMiddleware} schemaMiddlewareComponent - The middleware component. * @param {Object|Promise} schema - The GSS schema to validate against, or a promise that will resolve to same. * @param {Object} req - The Express request object. * @param {Object} res - The Express response object. From 61d2cf4bc08cca11d900f19be8a85a716e4767c6 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Thu, 19 Sep 2019 16:26:21 +0200 Subject: [PATCH 11/27] GPII-4022: Removed model transformation circuitry from schema holder per feedback from Antranig. --- src/js/common/schemaHolder.js | 45 +++-------- tests/js/common/schema-holder-tests.js | 108 +++++++++++++++---------- 2 files changed, 79 insertions(+), 74 deletions(-) diff --git a/src/js/common/schemaHolder.js b/src/js/common/schemaHolder.js index 14b8385..7cc3698 100644 --- a/src/js/common/schemaHolder.js +++ b/src/js/common/schemaHolder.js @@ -28,28 +28,8 @@ return schema; }; - gpii.schema.schemaHolder.incorporateSubcomponentSchemas = function (that, baseSchema) { - var subComponents = fluid.queryIoCSelector(that, "gpii.schema.schemaHolder", true); - fluid.each(subComponents, function (subComponent) { - // getSchema is asynchronous, so we have to use a "promise chain" to handle this. - that.events.incorporateSubcomponentSchemas.addListener(function (baseSchema) { - var singleComponentSchemaPromise = fluid.promise(); - subComponent.getSchema().then( - function (subComponentSchema) { - try { - var mergedSchema = fluid.model.transformWithRules({ baseSchema: baseSchema, toMerge: subComponentSchema }, that.options.rules.mergeSubcomponentSchema); - singleComponentSchemaPromise.resolve(mergedSchema); - } - catch (error) { - singleComponentSchemaPromise.reject(error); - } - }, - singleComponentSchemaPromise.reject - ); - return singleComponentSchemaPromise; - }); - }); - return fluid.promise.fireTransformEvent(that.events.incorporateSubcomponentSchemas, baseSchema); + gpii.schema.schemaHolder.incorporateSubcomponentSchemas = function (parentSchemaHolder, originalSchema) { + return originalSchema; }; fluid.defaults("gpii.schema.schemaHolder", { @@ -57,15 +37,8 @@ mergePolicy: { "rules.mergeSubcomponentSchema": "nomerge" }, - rules: { - // By default, simply ignore any child schemas. - mergeSubcomponentSchema: { - "": "baseSchema" - } - }, events: { - generateSchema: null, - incorporateSubcomponentSchemas: null + generateSchema: null }, members: { generatedSchema: false, @@ -76,6 +49,10 @@ additionalProperties: true }, invokers: { + getChildSchemaHolders: { + funcName: "fluid.queryIoCSelector", + args: ["{that}", "gpii.schema.schemaHolder", true] + }, getSchema: { funcName: "gpii.schema.schemaHolder.generateIfNeeded", args: ["{that}"] @@ -83,6 +60,10 @@ generateSchema: { funcName: "fluid.promise.fireTransformEvent", args: ["{that}.events.generateSchema"] + }, + incorporateSubcomponentSchemas: { + funcName: "gpii.schema.schemaHolder.incorporateSubcomponentSchemas", + args: ["{that}", "{arguments}.0"] // parentSchemaHolder, schemaToDate } }, listeners: { @@ -93,8 +74,8 @@ }, "generateSchema.incorporateComponentSchemas": { priority: "after:getOptions", - funcName: "gpii.schema.schemaHolder.incorporateSubcomponentSchemas", - args: ["{that}", "{arguments}.0"] + func: "{that}.incorporateSubcomponentSchemas", + args: ["{arguments}.0"] }, "generateSchema.cacheSchema": { priority: "last", diff --git a/tests/js/common/schema-holder-tests.js b/tests/js/common/schema-holder-tests.js index 711b559..416d735 100644 --- a/tests/js/common/schema-holder-tests.js +++ b/tests/js/common/schema-holder-tests.js @@ -23,22 +23,35 @@ var jqUnit = jqUnit || {}; } }); - // This transform is a crude demonstration of what we might do with the LSR, where we need to store multiple - // settings in a generated supportedSettings block. - fluid.defaults("gpii.tests.schema.schemaHolder.transforms.mergeChild", { - gradeNames: ["fluid.transformFunction"] - }); + fluid.registerNamespace("gpii.tests.schema.schemaHolder.parent"); + gpii.tests.schema.schemaHolder.parent.incorporateSubcomponentSchemas = function (parentSchemaHolder, schemaToDate) { + var outerPromise = fluid.promise(); - fluid.registerNamespace("gpii.tests.schema.schemaHolder.transforms"); - gpii.tests.schema.schemaHolder.transforms.mergeChild = function (transformSpec, transformer) { - if (!transformSpec.toMerge) { - fluid.fail("mergeChild transform requires a `toMerge` inputPath.", transformSpec); - } - var toMerge = transformer.expand(transformSpec.toMerge); - // TODO: pass both components rather than just their schemas, so that we can use some aspect of the component - // rather than the "title" property of the schema. - var outputPath = fluid.model.composePaths(transformer.outputPrefix, toMerge.title); - transformer.applier.change(outputPath, toMerge); + var modifiedSchema = fluid.copy(schemaToDate); + var schemaModificationPromises = []; + var childSchemaHolders = parentSchemaHolder.getChildSchemaHolders(); + fluid.each(childSchemaHolders, function (childSchemaHolder) { + schemaModificationPromises.push(function () { + var schemaMungingPromise = fluid.promise(); + childSchemaHolder.getSchema().then( + function (childSchema) { + fluid.set(modifiedSchema, ["properties", childSchemaHolder.options.name], childSchema); + schemaMungingPromise.resolve(); + }, + schemaMungingPromise.reject + ); + return schemaMungingPromise; + }); + }); + var schemaMungingSequence = fluid.promise.sequence(schemaModificationPromises); + schemaMungingSequence.then( + function () { + outerPromise.resolve(modifiedSchema); + }, + outerPromise.reject + ); + + return outerPromise; }; // A schema holder with multiple children. @@ -50,38 +63,56 @@ var jqUnit = jqUnit || {}; parentProperty: {type: "string"} } }, - rules: { - mergeSubcomponentSchema: { - "": "baseSchema", - properties: { - "": "baseSchema.properties", - transform: { - type: "gpii.tests.schema.schemaHolder.transforms.mergeChild", - toMerge: "toMerge" - } - } + invokers: { + incorporateSubcomponentSchemas: { + "funcName": "gpii.tests.schema.schemaHolder.parent.incorporateSubcomponentSchemas" } }, components: { oneChild: { type: "gpii.tests.schema.schemaHolder", options: { - schema: { - title: "oldest child" - } + name: "oldest child" } }, anotherChild: { type: "gpii.tests.schema.schemaHolder", options: { - schema: { - title: "youngest child" - } + name: "youngest child" } } } }); + fluid.registerNamespace("gpii.tests.schema.schemaHolder.grandparent"); + gpii.tests.schema.schemaHolder.grandparent.incorporateSubcomponentSchemas = function (grandparentSchemaHolder, schemaToDate) { + var outerPromise = fluid.promise(); + var modifiedSchema = fluid.copy(schemaToDate); + var schemaMungingPromises = []; + var childSchemaHolders = grandparentSchemaHolder.getChildSchemaHolders(); + fluid.each(childSchemaHolders, function (childSchemaHolder) { + schemaMungingPromises.push(function () { + var schemaMungingPromise = fluid.promise(); + childSchemaHolder.getSchema().then( + function (childSchema) { + fluid.set(modifiedSchema, ["properties", "child"], childSchema); + schemaMungingPromise.resolve(); + }, + schemaMungingPromise.reject + ); + return schemaMungingPromise; + }); + }); + var schemaMungingSequence = fluid.promise.sequence(schemaMungingPromises); + schemaMungingSequence.then( + function () { + outerPromise.resolve(modifiedSchema); + }, + outerPromise.reject + ); + return outerPromise; + }; + // A schema holder with a single child and multiple grandchildren. fluid.defaults("gpii.tests.schema.schemaHolder.grandparent", { gradeNames: ["gpii.schema.schemaHolder"], @@ -91,13 +122,10 @@ var jqUnit = jqUnit || {}; grandParentProperty: {type: "string"} } }, - rules: { - mergeSubcomponentSchema: { - "": "baseSchema", - // We use a simpler approach here as we only expect one child component. - properties: { - child: "toMerge" - } + subComponentGrade: "gpii.schema.schemaHolder", + invokers: { + incorporateSubcomponentSchemas: { + funcName: "gpii.tests.schema.schemaHolder.grandparent.incorporateSubcomponentSchemas" } }, components: { @@ -169,7 +197,6 @@ var jqUnit = jqUnit || {}; "oldest child": { "$schema": "gss-v7-full#", additionalProperties: true, - title: "oldest child", type: "object", properties: { childProperty: {type: "string"} @@ -177,7 +204,6 @@ var jqUnit = jqUnit || {}; }, "youngest child": { "$schema": "gss-v7-full#", - title: "youngest child", additionalProperties: true, type: "object", properties: { @@ -208,7 +234,6 @@ var jqUnit = jqUnit || {}; "type": "string" }, "oldest child": { - "title": "oldest child", "type": "object", "properties": { "childProperty": { @@ -219,7 +244,6 @@ var jqUnit = jqUnit || {}; "additionalProperties": true }, "youngest child": { - "title": "youngest child", "type": "object", "properties": { "childProperty": { From 49d66f9825dfd6d495c473b2d245bbbd1cb79ce2 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Mon, 23 Sep 2019 11:46:16 +0200 Subject: [PATCH 12/27] GPII-4022: Fixed issues with Headless Chrome Testem launcher. --- package.json | 2 +- tests/testem.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 85215c6..9e7505e 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,6 @@ "nyc": "12.0.2", "request": "2.88.0", "rimraf": "2.6.3", - "testem": "2.16.0" + "testem": "2.17.0" } } diff --git a/tests/testem.js b/tests/testem.js index 5b160bb..9343900 100644 --- a/tests/testem.js +++ b/tests/testem.js @@ -27,7 +27,9 @@ var testemComponent = gpii.testem.instrumentation({ gated: "/gated" }, testemOptions: { - skip: "PhantomJS,Safari,IE,Chrome" // Testem now has a "Chrome Headless" launcher built in, so we disable the headed version. + // Disable Headless Chrome we can figure out a solution to this issue: https://issues.gpii.net/browse/GPII-4064 + // Running Testem with the HEADLESS environment variable still works, and still runs headless. + skip: "PhantomJS,Safari,IE,Headless Chrome" }, components: { express: { From b97f472c4eaf89fa7a6b033b67129c83f8944ae4 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Mon, 23 Sep 2019 11:47:00 +0200 Subject: [PATCH 13/27] GPII-4022: Renamed schema generation promise chaining event to avoid naming confusion with the invoker that triggers the chain. --- src/js/common/schemaHolder.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/js/common/schemaHolder.js b/src/js/common/schemaHolder.js index 7cc3698..7264386 100644 --- a/src/js/common/schemaHolder.js +++ b/src/js/common/schemaHolder.js @@ -34,15 +34,11 @@ fluid.defaults("gpii.schema.schemaHolder", { gradeNames: ["fluid.component"], - mergePolicy: { - "rules.mergeSubcomponentSchema": "nomerge" - }, events: { - generateSchema: null + onGenerateSchema: null }, members: { - generatedSchema: false, - subComponentSchemaPromises: [] + generatedSchema: false }, schema: { $schema: "gss-v7-full#", @@ -59,7 +55,7 @@ }, generateSchema: { funcName: "fluid.promise.fireTransformEvent", - args: ["{that}.events.generateSchema"] + args: ["{that}.events.onGenerateSchema."] }, incorporateSubcomponentSchemas: { funcName: "gpii.schema.schemaHolder.incorporateSubcomponentSchemas", From 71dc2f89f2df8bcc4e0da0c3fbf4eec500219018 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Mon, 23 Sep 2019 14:07:47 +0200 Subject: [PATCH 14/27] GPII-4022: Added the new "schema holder" grade to the top-level index.js. --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index c458001..6b20ee1 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,7 @@ fluid.module.register("gpii-json-schema", __dirname, require); require("./src/js/common/gss-metaschema"); require("./src/js/common/validator"); require("./src/js/common/validation-errors"); +require("./src/js/common/schemaHolder"); require("./src/js/common/schemaValidatedComponent"); require("./src/js/common/schemaValidatedModelComponent"); From a4d83a4d6778d8c9bda02cbb4fd9a49c29cc153d Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Mon, 23 Sep 2019 14:13:32 +0200 Subject: [PATCH 15/27] GPII-4022: Added "schema holder" tests to "common" rollup and fix errors following recent refactor. --- src/js/common/schemaHolder.js | 8 ++++---- tests/js/common/index.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/js/common/schemaHolder.js b/src/js/common/schemaHolder.js index 7264386..e03e2e1 100644 --- a/src/js/common/schemaHolder.js +++ b/src/js/common/schemaHolder.js @@ -55,7 +55,7 @@ }, generateSchema: { funcName: "fluid.promise.fireTransformEvent", - args: ["{that}.events.onGenerateSchema."] + args: ["{that}.events.onGenerateSchema"] }, incorporateSubcomponentSchemas: { funcName: "gpii.schema.schemaHolder.incorporateSubcomponentSchemas", @@ -63,17 +63,17 @@ } }, listeners: { - "generateSchema.getOptions": { + "onGenerateSchema.getOptions": { priority: "first", funcName: "fluid.identity", args: ["{that}.options.schema"] }, - "generateSchema.incorporateComponentSchemas": { + "onGenerateSchema.incorporateComponentSchemas": { priority: "after:getOptions", func: "{that}.incorporateSubcomponentSchemas", args: ["{arguments}.0"] }, - "generateSchema.cacheSchema": { + "onGenerateSchema.cacheSchema": { priority: "last", funcName: "gpii.schema.schemaHolder.cacheSchema", args: ["{that}", "{arguments}.0"] diff --git a/tests/js/common/index.js b/tests/js/common/index.js index 4532b50..712f321 100644 --- a/tests/js/common/index.js +++ b/tests/js/common/index.js @@ -2,6 +2,7 @@ "use strict"; require("./metaschema-tests"); require("./orderedStringify-tests"); +require("./schema-holder-tests"); require("./schema-validated-modelComponent-tests"); require("./schema-validated-component-tests"); require("./schema-validated-component-pre-potentia-ii-tests"); From a78a1a6d64bef29ed51da47a824ff8c31e32ee40 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Mon, 30 Sep 2019 11:50:43 +0200 Subject: [PATCH 16/27] GPII-4022: Committed broken work in progress on "resource loading" refactor to get input. --- index.js | 1 - package.json | 18 +- src/js/client/errorBinder.js | 69 ++-- src/js/common/validation-errors.js | 39 -- src/js/common/validator.js | 10 +- src/js/server/kettleValidation.js | 11 +- src/js/server/schemaValidationMiddleware.js | 11 +- src/messages/validation_errors_en_us.json | 30 ++ tests/browser-fixtures/all-tests.html | 5 +- tests/browser-fixtures/errorBinder-demo.html | 27 +- tests/browser-fixtures/errorBinder-tests.html | 21 +- .../js/errorBinder-test-fixtures.js | 11 - .../browser-fixtures/js/errorBinder-tests.js | 9 +- .../js/messageLoadingRunner.js | 30 ++ ...dated-component-pre-potentia-ii-tests.html | 1 - .../schema-validated-component-tests.html | 3 +- ...schema-validated-modelComponent-tests.html | 1 - .../validator-global-component-tests.html | 1 - .../validator-static-function-tests.html | 5 +- tests/js/common/index.js | 17 +- .../validator-static-function-testDefs.js | 361 ++++++++++++++++++ .../common/validator-static-function-tests.js | 357 ----------------- tests/testem.js | 19 +- 23 files changed, 550 insertions(+), 507 deletions(-) delete mode 100644 src/js/common/validation-errors.js create mode 100644 src/messages/validation_errors_en_us.json create mode 100644 tests/browser-fixtures/js/messageLoadingRunner.js create mode 100644 tests/js/common/validator-static-function-testDefs.js delete mode 100644 tests/js/common/validator-static-function-tests.js diff --git a/index.js b/index.js index 6b20ee1..274720c 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,6 @@ fluid.module.register("gpii-json-schema", __dirname, require); // Require all of the server-side components at once. require("./src/js/common/gss-metaschema"); require("./src/js/common/validator"); -require("./src/js/common/validation-errors"); require("./src/js/common/schemaHolder"); require("./src/js/common/schemaValidatedComponent"); require("./src/js/common/schemaValidatedModelComponent"); diff --git a/package.json b/package.json index 9e7505e..ed441c2 100644 --- a/package.json +++ b/package.json @@ -17,27 +17,27 @@ "author": "Tony Atkins ", "license": "BSD-3-Clause", "dependencies": { - "ajv": "6.10.0", + "ajv": "6.10.2", "gpii-binder": "1.0.5", "gpii-express": "1.0.15", - "gpii-handlebars": "1.1.4", - "infusion": "3.0.0-dev.20190507T155813Z.4781871fd.FLUID-6148", + "gpii-handlebars": "2.1.0-dev.20190923T144705Z.38550ff.GPII-4100", + "infusion": "3.0.0-dev.20190926T213421Z.e84b3cf0e.FLUID-6148", "kettle": "1.11.0" }, "devDependencies": { - "eslint": "6.0.0", + "eslint": "6.5.0", "eslint-config-fluid": "1.3.0", "foundation-sites": "6.4.1", "gpii-grunt-lint-all": "1.0.5", - "gpii-testem": "2.1.10-dev.20190404T122608Z.b51705e.GPII-3457", + "gpii-testem": "2.1.10", "grunt": "1.0.4", - "handlebars": "4.1.2", - "markdown-it": "8.4.2", + "handlebars": "4.4.0", + "markdown-it": "10.0.0", "mkdirp": "0.5.1", "node-jqunit": "1.1.8", - "nyc": "12.0.2", + "nyc": "14.1.1", "request": "2.88.0", - "rimraf": "2.6.3", + "rimraf": "3.0.0", "testem": "2.17.0" } } diff --git a/src/js/client/errorBinder.js b/src/js/client/errorBinder.js index 471ff42..a9fac27 100644 --- a/src/js/client/errorBinder.js +++ b/src/js/client/errorBinder.js @@ -18,7 +18,7 @@ // The base component used to actually display validation errors. fluid.defaults("gpii.schema.client.errorBinder", { - gradeNames: ["gpii.schema.modelComponent"], + gradeNames: ["gpii.schema.modelComponent", "gpii.handlebars.templateAware.serverResourceAware"], errorBindings: "{that}.options.bindings", selectors: { "fieldError": ".fieldError" @@ -29,21 +29,30 @@ model: { validationResults: {} }, - components: { - renderer: { - type: "gpii.handlebars.renderer" - } - }, invokers: { renderErrors: { funcName: "gpii.schema.client.errorAwareForm.renderErrors", - args: ["{that}", "{renderer}"] // renderer + args: ["{that}", "{gpii.handlebars.renderer}"] // renderer } }, - modelListeners: { - validationResults: { - func: "{that}.renderErrors", - excludeSource: "init" + components: { + // We have to wait to render until the renderer is available, but also reload if our templates change. + gatedModelWatcher: { + type: "fluid.modelComponent", + createOnEvent: "{that}.events.onRendererAvailable", + options: { + model: { + messages: "{gpii.schema.client.errorBinder}.model.messages", + templates: "{gpii.schema.client.errorBinder}.model.templates", + validationResults: "{gpii.schema.client.errorBinder}.model.validationResults" + }, + modelListeners: { + validationResults: { + func: "{gpii.schema.client.errorBinder}.renderErrors", + excludeSource: "init" + } + } + } } } }); @@ -62,7 +71,7 @@ // We need to ensure that both our own markup and the field errors are rendered before we fire `onMarkupRendered`. gpii.schema.client.errorAwareForm.renderErrors = function (that, renderer) { - var templateExists = fluid.get(that, ["model", "templates", "pages", that.options.templateKeys.inlineError]); + var templateExists = fluid.get(renderer, ["model", "templates", "pages", that.options.templateKeys.inlineError]); if (templateExists && renderer) { // Get rid of any previous validation errors. that.locate("fieldError").remove(); @@ -122,25 +131,27 @@ } }, model: { - templates: "{renderer}.model.templates", message: false, validationResults: false }, - modelListeners: { - templates: [ - { - func: "{that}.renderInitialMarkup", - excludeSource: "init" - }, - { - func: "{that}.renderErrors", - excludeSource: "init" - } - ] - }, + components: { - renderer: { - type: "gpii.handlebars.renderer.serverAware" + // We have to wait to render until the renderer is available, but also reload if our templates change. + gatedModelWatcher: { + options: { + modelListeners: { + validationResults: [ + { + func: "{gpii.schema.client.errorBinder}.renderInitialMarkup", + excludeSource: "init" + }, + { + func: "{gpii.schema.client.errorBinder}.renderErrors", + excludeSource: "init" + } + ] + } + } }, success: { options: { @@ -171,6 +182,10 @@ listeners: { "onCreate.renderMarkup": { funcName: "fluid.identity" + }, + "onResourcesLoaded.log": { + funcName: "console.log", + args: ["Resources loaded..."] } } }); diff --git a/src/js/common/validation-errors.js b/src/js/common/validation-errors.js deleted file mode 100644 index 885ec1b..0000000 --- a/src/js/common/validation-errors.js +++ /dev/null @@ -1,39 +0,0 @@ -/* globals require */ -var fluid = fluid || require("infusion"); -(function (fluid) { - "use strict"; - - var gpii = fluid.registerNamespace("gpii"); - fluid.registerNamespace("gpii.schema.messages"); - - gpii.schema.messages.validationErrors = { - "gpii.schema.messages.validationErrors.additionalProperties": "The property must match the requirements for additional properties.", - "gpii.schema.messages.validationErrors.anyOf": "The value must match at least one valid format.", - "gpii.schema.messages.validationErrors.contains": "The array is missing one or more required values.", - "gpii.schema.messages.validationErrors.dependencies": "A dependency between two fields is not satisfied.", - "gpii.schema.messages.validationErrors.else": "An 'else' block in the schema does not match the supplied data.", - "gpii.schema.messages.validationErrors.enum": "The supplied value is not one of the allowed values (%error.rule.enum).", - "gpii.schema.messages.validationErrors.exclusiveMaximum": "The value must be less than %error.rule.exclusiveMaximum characters long.", - "gpii.schema.messages.validationErrors.exclusiveMinimum": "The value must be more than %error.rule.exclusiveMinimum characters long.", - "gpii.schema.messages.validationErrors.format": "The supplied string does not match the specified format (%error.rule.format).", - "gpii.schema.messages.validationErrors.generalFailure": "The data you have supplied is invalid.", - "gpii.schema.messages.validationErrors.if": "An 'if' block in the schema does not match the supplied data.", - "gpii.schema.messages.validationErrors.maxItems": "The supplied array must contain less than %error.rule.maxItems items.", - "gpii.schema.messages.validationErrors.maxLength": "The value must be %error.rule.maxLength characters or less long.", - "gpii.schema.messages.validationErrors.maxProperties": "The object can only contain %error.rule.maxProperties properties.", - "gpii.schema.messages.validationErrors.maximum": "The value must be less than %error.rule.maximum.", - "gpii.schema.messages.validationErrors.minItems": "The value must contain at least %error.rule.minItems.", - "gpii.schema.messages.validationErrors.minLength": "The value must be %error.rule.minLength characters or more long.", - "gpii.schema.messages.validationErrors.minProperties": "The object supplied must contain at least %error.rule.minProperties properties.", - "gpii.schema.messages.validationErrors.minimum": "The value must be at least %error.rule.minimum.", - "gpii.schema.messages.validationErrors.multipleOf": "The supplied value must be a multiple of %error.rule.multipleOf.", - "gpii.schema.messages.validationErrors.not": "The supplied data is disallowed by a 'not' block in the schema.", - "gpii.schema.messages.validationErrors.oneOf": "The value must match exactly one valid format.", - "gpii.schema.messages.validationErrors.pattern": "The supplied string does not match the expected pattern.", - "gpii.schema.messages.validationErrors.propertyNames": "The supplied property name does not match the allowed names.", - "gpii.schema.messages.validationErrors.required": "This value is required.", - "gpii.schema.messages.validationErrors.then": "A 'then' block in the schema does not match the supplied data.", - "gpii.schema.messages.validationErrors.type": "The value supplied should be a(n) %error.rule.type.", - "gpii.schema.messages.validationErrors.uniqueItems": "All items in the array must be unique." - }; -})(fluid); diff --git a/src/js/common/validator.js b/src/js/common/validator.js index 9f81bad..d4937af 100644 --- a/src/js/common/validator.js +++ b/src/js/common/validator.js @@ -6,7 +6,6 @@ var fluid = fluid || require("infusion"); if (fluid.require) { require("./gss-metaschema"); - require("./validation-errors"); require("./orderedStringify"); } @@ -522,21 +521,20 @@ var fluid = fluid || require("infusion"); * A function to translate/localise validation errors. * * If you want to pass a custom message bundle to this function, it should only contain top-level elements, see - * ./src/js/validation-errors.js in this package for an example. + * the ./src/messages/ directly in this package for concrete examples. * * @param {Array} validationErrors - An array of validation errors, see `gpii.schema.validator.standardiseAjvErrors` for details. * @param {Any} validatedData - The (optional) data that was validated. - * @param {Object} messages - An (optional) map of message templates (see above). Defaults to the message bundle provided by this package. + * @param {Object} messageBundle - An (optional) map of message templates (see above). Defaults to the message bundle provided by this package. * @param {Object} localisationTransform - An optional set of rules that control what information is available when localising validation errors (see above). * @return {Array} - The validation errors, with all message keys replaced with localised strings. * */ - gpii.schema.validator.localiseErrors = function (validationErrors, validatedData, messages, localisationTransform) { - messages = messages || gpii.schema.messages.validationErrors; + gpii.schema.validator.localiseErrors = function (validationErrors, validatedData, messageBundle, localisationTransform) { localisationTransform = localisationTransform || gpii.schema.validator.defaultLocalisationTransformRules; var localisedErrors = fluid.transform(validationErrors, function (validationError) { var messageKey = fluid.get(validationError, "message"); - var messageTemplate = messageKey && fluid.get(messages, [messageKey]); // We use the segment format because the keys contain dots. + var messageTemplate = messageKey && fluid.get(messageBundle, [messageKey]); // We use the segment format because the keys contain dots. if (messageTemplate) { var data = validatedData && fluid.get(validatedData, validationError.dataPath); var localisationContext = fluid.model.transformWithRules({ data: data, error: validationError}, localisationTransform); diff --git a/src/js/server/kettleValidation.js b/src/js/server/kettleValidation.js index a16e93c..0c81d5e 100644 --- a/src/js/server/kettleValidation.js +++ b/src/js/server/kettleValidation.js @@ -3,6 +3,8 @@ var fluid = require("infusion"); var gpii = fluid.registerNamespace("gpii"); +fluid.require("%gpii-handlebars"); + fluid.registerNamespace("gpii.schema.kettle.validator"); /** @@ -28,7 +30,8 @@ gpii.schema.kettle.validator.validateRequest = function (kettleValidator, global validationPromise.resolve(); } else { - var localisedErrors = gpii.schema.validator.localiseErrors(validationResults.errors, toValidate, kettleValidator.model.messages, kettleValidator.options.localisationTransform); + var messageBundle = gpii.handlebars.i18n.deriveMessageBundleFromRequest(requestHandler.req, kettleValidator.model.messageBundles, kettleValidator.options.defaultLocale); + var localisedErrors = gpii.schema.validator.localiseErrors(validationResults.errors, toValidate, messageBundle, kettleValidator.options.localisationTransform); var localisedPayload = fluid.copy(validationResults); localisedPayload.errors = localisedErrors; @@ -45,8 +48,12 @@ gpii.schema.kettle.validator.validateRequest = function (kettleValidator, global fluid.defaults("gpii.schema.kettle.validator", { gradeNames: ["kettle.middleware", "fluid.modelComponent"], schemaHash: "@expand:gpii.schema.hashSchema({that}.options.requestSchema)", + defaultLocale: "en_US", + messageDirs: { + validation: "%gpii-json-schema/src/messages" + }, model: { - messages: gpii.schema.messages.validationErrors + messageBundles: "@expand:gpii.handlebars.i18n.loadMessageBundles({that}.options.messageDirs)" }, localisationTransform: { "": "" diff --git a/src/js/server/schemaValidationMiddleware.js b/src/js/server/schemaValidationMiddleware.js index 6823f13..b68bf28 100644 --- a/src/js/server/schemaValidationMiddleware.js +++ b/src/js/server/schemaValidationMiddleware.js @@ -16,6 +16,8 @@ fluid.registerNamespace("gpii.schema.validationMiddleware"); require("../common/validator"); require("../common/schemaValidatedComponent"); +fluid.require("%gpii-handlebars"); + /** * * The core of both the gpii-express and kettle validation middleware. Transforms an incoming request and validates the @@ -49,7 +51,8 @@ gpii.schema.validationMiddleware.rejectOrForward = function (validatorComponent next(); } else { - var localisedErrors = gpii.schema.validator.localiseErrors(validationResults.errors, toValidate, schemaMiddlewareComponent.model.messages, schemaMiddlewareComponent.options.localisationTransform); + var messageBundle = gpii.handlebars.i18n.deriveMessageBundleFromRequest(req, schemaMiddlewareComponent.model.messageBundles, schemaMiddlewareComponent.options.defaultLocale); + var localisedErrors = gpii.schema.validator.localiseErrors(validationResults.errors, toValidate, messageBundle, schemaMiddlewareComponent.options.localisationTransform); var localisedPayload = fluid.copy(validationResults); localisedPayload.errors = localisedErrors; localisedPayload.statusCode = schemaMiddlewareComponent.options.invalidStatusCode; @@ -70,6 +73,7 @@ gpii.schema.validationMiddleware.rejectOrForward = function (validatorComponent fluid.defaults("gpii.schema.validationMiddleware", { gradeNames: ["fluid.modelComponent", "gpii.schema.component", "gpii.express.middleware"], namespace: "validationMiddleware", // A namespace that can be used to order other middleware relative to this component. + defaultLocale: "en_US", invalidStatusCode: 400, inputSchema: { "$schema": "gss-v7-full#" @@ -78,8 +82,11 @@ fluid.defaults("gpii.schema.validationMiddleware", { localisationTransform: { "": "" }, + messageDirs: { + validation: "%gpii-json-schema/src/messages" + }, model: { - messages: gpii.schema.messages.validationErrors + messageBundles: "@expand:gpii.handlebars.i18n.loadMessageBundles({that}.options.messageDirs)" }, // We prevent merging of individual options, but allow them to be individually replaced. mergeOptions: { diff --git a/src/messages/validation_errors_en_us.json b/src/messages/validation_errors_en_us.json new file mode 100644 index 0000000..67c30af --- /dev/null +++ b/src/messages/validation_errors_en_us.json @@ -0,0 +1,30 @@ +{ + "gpii.schema.messages.validationErrors.additionalProperties": "The property must match the requirements for additional properties.", + "gpii.schema.messages.validationErrors.anyOf": "The value must match at least one valid format.", + "gpii.schema.messages.validationErrors.contains": "The array is missing one or more required values.", + "gpii.schema.messages.validationErrors.dependencies": "A dependency between two fields is not satisfied.", + "gpii.schema.messages.validationErrors.else": "An 'else' block in the schema does not match the supplied data.", + "gpii.schema.messages.validationErrors.enum": "The supplied value is not one of the allowed values (%error.rule.enum).", + "gpii.schema.messages.validationErrors.exclusiveMaximum": "The value must be less than %error.rule.exclusiveMaximum characters long.", + "gpii.schema.messages.validationErrors.exclusiveMinimum": "The value must be more than %error.rule.exclusiveMinimum characters long.", + "gpii.schema.messages.validationErrors.format": "The supplied string does not match the specified format (%error.rule.format).", + "gpii.schema.messages.validationErrors.generalFailure": "The data you have supplied is invalid.", + "gpii.schema.messages.validationErrors.if": "An 'if' block in the schema does not match the supplied data.", + "gpii.schema.messages.validationErrors.maxItems": "The supplied array must contain less than %error.rule.maxItems items.", + "gpii.schema.messages.validationErrors.maxLength": "The value must be %error.rule.maxLength characters or less long.", + "gpii.schema.messages.validationErrors.maxProperties": "The object can only contain %error.rule.maxProperties properties.", + "gpii.schema.messages.validationErrors.maximum": "The value must be less than %error.rule.maximum.", + "gpii.schema.messages.validationErrors.minItems": "The value must contain at least %error.rule.minItems.", + "gpii.schema.messages.validationErrors.minLength": "The value must be %error.rule.minLength characters or more long.", + "gpii.schema.messages.validationErrors.minProperties": "The object supplied must contain at least %error.rule.minProperties properties.", + "gpii.schema.messages.validationErrors.minimum": "The value must be at least %error.rule.minimum.", + "gpii.schema.messages.validationErrors.multipleOf": "The supplied value must be a multiple of %error.rule.multipleOf.", + "gpii.schema.messages.validationErrors.not": "The supplied data is disallowed by a 'not' block in the schema.", + "gpii.schema.messages.validationErrors.oneOf": "The value must match exactly one valid format.", + "gpii.schema.messages.validationErrors.pattern": "The supplied string does not match the expected pattern.", + "gpii.schema.messages.validationErrors.propertyNames": "The supplied property name does not match the allowed names.", + "gpii.schema.messages.validationErrors.required": "This value is required.", + "gpii.schema.messages.validationErrors.then": "A 'then' block in the schema does not match the supplied data.", + "gpii.schema.messages.validationErrors.type": "The value supplied should be a(n) %error.rule.type.", + "gpii.schema.messages.validationErrors.uniqueItems": "All items in the array must be unique." +} diff --git a/tests/browser-fixtures/all-tests.html b/tests/browser-fixtures/all-tests.html index c3a358d..322a6d0 100644 --- a/tests/browser-fixtures/all-tests.html +++ b/tests/browser-fixtures/all-tests.html @@ -10,8 +10,8 @@ diff --git a/tests/browser-fixtures/errorBinder-demo.html b/tests/browser-fixtures/errorBinder-demo.html index 3387e90..ae88ff8 100644 --- a/tests/browser-fixtures/errorBinder-demo.html +++ b/tests/browser-fixtures/errorBinder-demo.html @@ -6,19 +6,7 @@ - - - - - - - - - - - - - + @@ -39,6 +27,7 @@ + @@ -46,6 +35,7 @@ + @@ -58,15 +48,14 @@ - - - - - + + + + + -
diff --git a/tests/browser-fixtures/errorBinder-tests.html b/tests/browser-fixtures/errorBinder-tests.html index 7caa1bc..daeef99 100644 --- a/tests/browser-fixtures/errorBinder-tests.html +++ b/tests/browser-fixtures/errorBinder-tests.html @@ -2,23 +2,10 @@ "Error Binder" Tests - - + - - - - - - - - - - - - - + @@ -45,6 +32,7 @@ + @@ -52,6 +40,7 @@ + @@ -74,8 +63,6 @@ - - diff --git a/tests/browser-fixtures/js/errorBinder-test-fixtures.js b/tests/browser-fixtures/js/errorBinder-test-fixtures.js index 28ec46e..5f2140f 100644 --- a/tests/browser-fixtures/js/errorBinder-test-fixtures.js +++ b/tests/browser-fixtures/js/errorBinder-test-fixtures.js @@ -1,7 +1,5 @@ -/* global fluid */ (function (fluid) { "use strict"; - var gpii = fluid.registerNamespace("gpii"); fluid.defaults("gpii.tests.schema.errorBinder", { gradeNames: ["gpii.schema.client.errorAwareForm"], @@ -66,15 +64,6 @@ }, model: { }, - components: { - renderer: { - options: { - model: { - messages: gpii.schema.messages.validationErrors - } - } - } - }, bindings: { // We use both styles of bindings to confirm that they each work with the `errorBinder`. shallowlyRequired: { diff --git a/tests/browser-fixtures/js/errorBinder-tests.js b/tests/browser-fixtures/js/errorBinder-tests.js index 34eb3e8..77739c1 100644 --- a/tests/browser-fixtures/js/errorBinder-tests.js +++ b/tests/browser-fixtures/js/errorBinder-tests.js @@ -2,6 +2,7 @@ var fluid = fluid || {}; (function (fluid, $, jqUnit) { "use strict"; + fluid.setLogging(true); var gpii = fluid.registerNamespace("gpii"); fluid.registerNamespace("gpii.tests.schema.errorBinder"); @@ -42,6 +43,7 @@ var fluid = fluid || {}; { name: "Confirm that initial client-side validation errors appear correctly after startup...", sequence: [ + // TODO: Convert to sequence grade. { func: "{testEnvironment}.events.constructFixtures.fire" }, { event: "{testEnvironment}.events.onFixturesReady", @@ -247,10 +249,9 @@ var fluid = fluid || {}; createOnEvent: "constructFixtures", container: ".errorBinder-viewport", options: { - modelListeners: { - "templates": { - func: "{testEnvironment}.events.onFixturesReady.fire", - excludeSource: "init" + listeners: { + "onRendererAvailable.notifyParent": { + func: "{testEnvironment}.events.onFixturesReady.fire" } } } diff --git a/tests/browser-fixtures/js/messageLoadingRunner.js b/tests/browser-fixtures/js/messageLoadingRunner.js new file mode 100644 index 0000000..2d63584 --- /dev/null +++ b/tests/browser-fixtures/js/messageLoadingRunner.js @@ -0,0 +1,30 @@ +/* eslint-env browser */ +(function (fluid) { + "use strict"; + var gpii = fluid.registerNamespace("gpii"); + + fluid.registerNamespace("gpii.tests.schema.messageLoadingRunner"); + + // TODO: If this gets any more convoluted, convert to a test environment or other component hierarchy. + gpii.tests.schema.messageLoadingRunner.runTests = function (that) { + fluid.setGlobalValue("gpii.tests.schema.defaultMessageBundle", that.resources.messages.parsed); + gpii.tests.schema.validator.staticFunctionTests(); + }; + + fluid.defaults("gpii.tests.schema.messageLoadingRunner", { + gradeNames: ["fluid.resourceLoader"], + resources: { + messages: { + url: "/messages", + dataType: "json" + } + }, + listeners: { + "onResourcesLoaded.runTests": { + funcName: "gpii.tests.schema.messageLoadingRunner.runTests", + args: ["{that}"] + } + } + }); + gpii.tests.schema.messageLoadingRunner(); +})(fluid); diff --git a/tests/browser-fixtures/schema-validated-component-pre-potentia-ii-tests.html b/tests/browser-fixtures/schema-validated-component-pre-potentia-ii-tests.html index a9fe2d2..2e10635 100644 --- a/tests/browser-fixtures/schema-validated-component-pre-potentia-ii-tests.html +++ b/tests/browser-fixtures/schema-validated-component-pre-potentia-ii-tests.html @@ -17,7 +17,6 @@ - diff --git a/tests/browser-fixtures/schema-validated-component-tests.html b/tests/browser-fixtures/schema-validated-component-tests.html index a7d09c9..9c8edc4 100644 --- a/tests/browser-fixtures/schema-validated-component-tests.html +++ b/tests/browser-fixtures/schema-validated-component-tests.html @@ -17,7 +17,6 @@ - @@ -31,4 +30,4 @@

- \ No newline at end of file + diff --git a/tests/browser-fixtures/schema-validated-modelComponent-tests.html b/tests/browser-fixtures/schema-validated-modelComponent-tests.html index d5a4b6d..174de46 100644 --- a/tests/browser-fixtures/schema-validated-modelComponent-tests.html +++ b/tests/browser-fixtures/schema-validated-modelComponent-tests.html @@ -17,7 +17,6 @@ - diff --git a/tests/browser-fixtures/validator-global-component-tests.html b/tests/browser-fixtures/validator-global-component-tests.html index 5986443..9848883 100644 --- a/tests/browser-fixtures/validator-global-component-tests.html +++ b/tests/browser-fixtures/validator-global-component-tests.html @@ -19,7 +19,6 @@ - diff --git a/tests/browser-fixtures/validator-static-function-tests.html b/tests/browser-fixtures/validator-static-function-tests.html index 9a09309..8de2fbf 100644 --- a/tests/browser-fixtures/validator-static-function-tests.html +++ b/tests/browser-fixtures/validator-static-function-tests.html @@ -17,7 +17,6 @@ - @@ -29,6 +28,8 @@

- + + + diff --git a/tests/js/common/index.js b/tests/js/common/index.js index 712f321..51997e7 100644 --- a/tests/js/common/index.js +++ b/tests/js/common/index.js @@ -1,5 +1,8 @@ /* eslint-env node */ "use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + require("./metaschema-tests"); require("./orderedStringify-tests"); require("./schema-holder-tests"); @@ -7,4 +10,16 @@ require("./schema-validated-modelComponent-tests"); require("./schema-validated-component-tests"); require("./schema-validated-component-pre-potentia-ii-tests"); require("./validator-global-component-tests"); -require("./validator-static-function-tests"); + +// As the mechanisms for retrieving message bundles differ widely, each environment must define the standard namespaced +// message bundle used in the "common" static function tests. This is a simple definition for Node.js. + +fluid.require("%gpii-handlebars"); +fluid.registerNamespace("gpii.tests.schema"); + +gpii.tests.schema.defaultMessageBundle = gpii.handlebars.i18n.deriveMessageBundle(false, gpii.handlebars.i18n.loadMessageBundles({ + validation: "%gpii-json-schema/src/messages" +})); + +require("./validator-static-function-testDefs"); +gpii.tests.schema.validator.staticFunctionTests(); diff --git a/tests/js/common/validator-static-function-testDefs.js b/tests/js/common/validator-static-function-testDefs.js new file mode 100644 index 0000000..2e610ee --- /dev/null +++ b/tests/js/common/validator-static-function-testDefs.js @@ -0,0 +1,361 @@ +/* globals jqUnit, require */ +/* eslint-env browser */ +var fluid = fluid || {}; +var jqUnit = jqUnit || {}; + +(function (fluid, jqUnit) { + "use strict"; + if (!fluid.identity) { + fluid = require("infusion"); + jqUnit = require("node-jqunit"); + require("../../../src/js/common/validator"); + require("./validator-tests-ajv-errors-testDefs"); + } + + var gpii = fluid.registerNamespace("gpii"); + fluid.registerNamespace("gpii.tests.schema.validator"); + + gpii.tests.schema.validator.staticFunctionTests = function () { + jqUnit.module("Testing validator static functions."); + + jqUnit.test("Testing `gpii.schema.deriveRequiredProperties`.", function () { + jqUnit.assertDeepEq("We should be able to generate a list of required sub-properties.", ["foo", "bar"], gpii.schema.deriveRequiredProperties({ + foo: {required: true}, + bar: {required: true}, + baz: {} + })); + jqUnit.assertDeepEq("We should be able to handle an empty object.", [], gpii.schema.deriveRequiredProperties({})); + }); + + jqUnit.test("Testing `gpii.schema.gssToJsonSchema`.", function () { + var testDefs = { + allSorts: { + message: "We should be able to handle a full range of values.", + gssSchema: {properties: {foo: {enum: [[0, 1, 2], true, "a string", undefined]}}}, + expected: { + "$schema": "http://json-schema.org/draft-07/schema#", + properties: {foo: {enum: [[0, 1, 2], true, "a string", undefined]}} + } + }, + anyOf: { + message: "We should be able to handle array constructs like `anyOf`.", + gssSchema: {anyOf: [{properties: {foo: {required: true}}}, {properties: {errors: {required: true}}}]}, + expected: { + "$schema": "http://json-schema.org/draft-07/schema#", anyOf: [ + {required: ["foo"], properties: {foo: {}}}, + {required: ["errors"], properties: {errors: {}}} + ] + } + }, + emptySchema: { + message: "We should be able to handle an empty schema.", + gssSchema: {}, + expected: {"$schema": "http://json-schema.org/draft-07/schema#"} + }, + enumLabels: { + message: "We should be able to handle our custom `enumLabels` element.", + gssSchema: {properties: {foo: {enumLabels: ["foo"], enum: ["bar"]}}}, + expected: {"$schema": "http://json-schema.org/draft-07/schema#", properties: {foo: {enum: ["bar"]}}} + }, + hint: { + message: "We should be able to handle our custom `hint` element.", + gssSchema: {properties: {foo: {hint: "baz"}}}, + expected: {"$schema": "http://json-schema.org/draft-07/schema#", properties: {foo: {}}} + }, + errors: { + message: "We should be able to handle our custom `errors` element.", + gssSchema: {properties: {foo: {errors: {"": "baz"}}}}, + expected: {"$schema": "http://json-schema.org/draft-07/schema#", properties: {foo: {}}} + }, + specialPropertyNames: { + message: "We should not filter out properties that match our GSS keywords.", + gssSchema: {properties: {errors: {}, hint: {}}}, + expected: {"$schema": "http://json-schema.org/draft-07/schema#", properties: {errors: {}, hint: {}}} + }, + requiredField: { + message: "We should be able to handle a single required field.", + gssSchema: {properties: {foo: {required: true}}}, + expected: {"$schema": "http://json-schema.org/draft-07/schema#", required: ["foo"], properties: {foo: {}}} + }, + requiredFields: { + message: "We should be able to handle multiple required fields.", + gssSchema: {properties: {foo: {required: true}, bar: {required: true, type: "string"}}}, + expected: { + "$schema": "http://json-schema.org/draft-07/schema#", + required: ["foo", "bar"], + properties: {foo: {}, bar: {type: "string"}} + } + } + }; + + fluid.each(testDefs, function (testDef) { + jqUnit.assertDeepEq(testDef.message, testDef.expected, gpii.schema.gssToJsonSchema(testDef.gssSchema)); + }); + }); + + jqUnit.test("Testing `gpii.schema.removeEmptyItems`.", function () { + var testDefs = { + nonEmpty: {message: "Non-empty items should be preserved.", input: ["a", "string"], expected: ["a", "string"]}, + emptyStrings: { + message: "Empty strings should be removed.", + input: ["", "not empty", ""], + expected: ["not empty"] + }, + numbers: {message: "Numbers should be allowed.", input: [1, 2, 3], expected: [1, 2, 3]}, + undefined: { + message: "Undefined elements should be removed.", + input: [1, undefined, 2, undefined, 3], + expected: [1, 2, 3] + }, + null: {message: "Null elements should be removed.", input: [1, null, 2, null, 3], expected: [1, 2, 3]}, + invalidItems: { + message: "Elements other than strings or integers should be removed.", + input: [true, Math.PI, {}, []], + expected: [] + } + }; + fluid.each(testDefs, function (testDef) { + jqUnit.assertDeepEq(testDef.message, testDef.expected, testDef.input.filter(gpii.schema.removeEmptyItems)); + }); + }); + + jqUnit.test("Testing `gpii.schema.validator.errorHintForRule`.", function () { + var testDefs = { + defaultMessage: { + message: "We should be able to failover to a default message.", + gssSchema: {newRule: "shiny"}, + rulePath: ["newRule"], + defaultMessage: "This is the default message.", + expected: "This is the default message." + }, + ruleMessage: { + message: "We should be able to failover to one of the built-in keys for a given rule.", + gssSchema: {type: "text"}, + rulePath: ["type"], + defaultMessage: "This is the default message.", + expected: "gpii.schema.messages.validationErrors.type" + }, + shortForm: { + message: "We should be able to use a 'short form' error definition.", + gssSchema: {type: "text", "errors": "short-form-key"}, + rulePath: ["type"], + defaultMessage: "This is the default message.", + expected: "short-form-key" + }, + longFormByRule: { + message: "We should be able to use a 'long form' error definition for a single rule.", + gssSchema: {type: "text", "errors": {type: "long-form-by-rule-key"}}, + rulePath: ["type"], + defaultMessage: "This is the default message.", + expected: "long-form-by-rule-key" + }, + longFormFailover: { + message: "We should be able to use a 'long form' error definition for multiple rules.", + gssSchema: {type: "text", "errors": {"": "long-form-failover-key"}}, + rulePath: ["type"], + defaultMessage: "This is the default message.", + expected: "long-form-failover-key" + } + }; + fluid.each(testDefs, function (testDef) { + jqUnit.assertEquals(testDef.message, testDef.expected, gpii.schema.validator.errorHintForRule(testDef.rulePath, testDef.gssSchema, testDef.defaultMessage)); + }); + }); + + jqUnit.test("Testing `gpii.schema.validator.extractElDataPathSegmentsFromError`.", function () { + var testDefs = { + rootDataPath: { + message: "We should be able to handle the root dataPath.", + error: {dataPath: ""}, + expected: [] + }, + deepPath: { + message: "We should be able to handle a deep dataPath.", + error: {dataPath: ".deep.path.to.material"}, + expected: ["deep", "path", "to", "material"] + }, + requiredTopLevelField: { + message: "We should be able to handle a missing top-level required field.", + error: {dataPath: "", keyword: "required", params: {missingProperty: "requiredField"}}, + expected: ["requiredField"] + }, + requiredDeepField: { + message: "We should be able to handle a missing deep required field.", + error: {dataPath: ".deep", keyword: "required", params: {missingProperty: "requiredField"}}, + expected: ["deep", "requiredField"] + }, + dottedField: { + message: "We should be able to handle a dotted field in a dataPath.", + error: {"dataPath": "['dotted.field']"}, + expected: ["dotted.field"] + }, + dottedRequiredField: { + message: "We should be able to handle a missing required field with a dot in its key.", + error: { + "keyword": "required", + "dataPath": "", + "params": { + "missingProperty": "dotted.field" + } + }, + expected: ["dotted.field"] + }, + apostropheField: { + message: "We should be able to handle a dataPath with an escaped apostrophe", + error: { + "dataPath": "['don\\'t']" + }, + expected: ["don't"] + }, + complexMixture: { + message: "We should be able to handle complex interleaved data paths.", + error: { + "dataPath": ".settingsHandlers['configure'].supportedSettings['MagnificationMode'].schema" + }, + expected: ["settingsHandlers", "configure", "supportedSettings", "MagnificationMode", "schema"] + } + }; + fluid.each(testDefs, function (testDef) { + jqUnit.assertDeepEq(testDef.message, testDef.expected, gpii.schema.validator.extractElDataPathSegmentsFromError(testDef.error)); + }); + }); + + jqUnit.test("Testing `gpii.schema.validator.jsonPointerToElPath`.", function () { + var testDefs = { + withId: { + message: "We should be able to handle a pointer that includes an ID.", + input: "schema-id.json#/path/to/material", + expected: ["path", "to", "material"] + }, + relative: { + message: "We should be able to handle a relative pointer.", + input: "#/path/to/material", + expected: ["path", "to", "material"] + }, + arrayEntry: { + message: "We should be able to handle a pointer that contains a reference to an array element.", + input: "#/anyOf/1/type", + expected: ["anyOf", "1", "type"] + }, + root: { message: "We should be able to handle a pointer to the root of the object.", input: "#/", expected: [] } + }; + fluid.each(testDefs, function (testDef) { + jqUnit.assertDeepEq(testDef.message, testDef.expected, gpii.schema.validator.jsonPointerToElPath(testDef.input)); + }); + }); + + jqUnit.test("Testing `gpii.schema.validator.standardiseAjvErrors`.", function () { + fluid.each(gpii.tests.validator.ajvErrors, function (testDef) { + var standardisedErrors = gpii.schema.validator.standardiseAjvErrors(testDef.schema, testDef.input); + jqUnit.assertDeepEq(testDef.message, testDef.expected, standardisedErrors); + }); + + var expected = {isValid: true, errors: []}; + var noErrorOutput = gpii.schema.validator.standardiseAjvErrors({}, false); + jqUnit.assertDeepEq("We should be able to handle the case in which there are no AJV errors.", expected, noErrorOutput); + }); + + jqUnit.test("Testing `gpii.schema.validator.localiseErrors`.", function () { + var testDefs = { + defaultsNoData: { + message: "We should be able to resolve error message keys with only the defaults.", + errors: [{message: "gpii.schema.messages.validationErrors.required"}], + expected: [{message: "This value is required."}] + }, + defaultData: { + message: "We should be able to resolve error message keys with the defaults and the data being validated.", + toValidate: true, + errors: [{message: "gpii.schema.messages.validationErrors.required", dataPath: [""]}], + expected: [{message: "This value is required.", dataPath: [""]}] + }, + noErrors: { + message: "We should be able to handle the case in which there are no errors.", + errors: [], + expected: [] + }, + ruleInTemplate: { + message: "We should be able to reference information from the rule in an error template.", + errors: [{ + "dataPath": [], + "schemaPath": [ + "maxLength" + ], + "rule": { + "type": "string", + "maxLength": 2 + }, + "message": "gpii.schema.messages.validationErrors.maxLength" + }], + expected: [{ + "dataPath": [], + "schemaPath": [ + "maxLength" + ], + "rule": { + "type": "string", + "maxLength": 2 + }, + "message": "The value must be 2 characters or less long." + }] + }, + customMessageBundle: { + message: "We should be able to use a custom message bundle.", + messages: { + "gpii.schema.messages.validationErrors.maxLength": "The value is too long." + }, + errors: [{ + "message": "gpii.schema.messages.validationErrors.maxLength" + }], + expected: [{ + "message": "The value is too long." + }] + }, + dataInTemplate: { + message: "We should be able to use data in a custom error template.", + toValidate: {value: "the value itself"}, + messages: {"custom-template": "We have nothing to evaluate but %data.value."}, + errors: [{ + "dataPath": [], + "message": "custom-template" + }], + expected: [{ + "dataPath": [], + "message": "We have nothing to evaluate but the value itself." + }] + }, + rootValue: { + message: "We should be able to use a root data value.", + toValidate: "root", + messages: {"custom-template": "We have discovered the %data of the problem."}, + errors: [{ + "dataPath": [], + "message": "custom-template" + }], + expected: [{ + "dataPath": [], + "message": "We have discovered the root of the problem." + }] + }, + customTransform: { + message: "We should be able to supply our own localisation rules.", + toValidate: {old: "change"}, + messages: {"custom-template": "The only constant is %data.new."}, + localisationTransform: {data: {"new": "data.old"}}, + errors: [{ + "dataPath": [], + "message": "custom-template" + }], + expected: [{ + "dataPath": [], + "message": "The only constant is change." + }] + } + }; + fluid.each(testDefs, function (testDef) { + var messageBundle = fluid.extend({}, gpii.tests.schema.defaultMessageBundle, testDef.messages); + var output = gpii.schema.validator.localiseErrors(testDef.errors, testDef.toValidate, messageBundle, testDef.localisationTransform); + jqUnit.assertDeepEq(testDef.message, testDef.expected, output); + }); + }); + }; +})(fluid, jqUnit); diff --git a/tests/js/common/validator-static-function-tests.js b/tests/js/common/validator-static-function-tests.js deleted file mode 100644 index 9290dc4..0000000 --- a/tests/js/common/validator-static-function-tests.js +++ /dev/null @@ -1,357 +0,0 @@ -/* globals jqUnit, require */ -/* eslint-env browser */ -var fluid = fluid || {}; -var jqUnit = jqUnit || {}; - -(function (fluid, jqUnit) { - "use strict"; - if (!fluid.identity) { - fluid = require("infusion"); - jqUnit = require("node-jqunit"); - require("../../../src/js/common/validator"); - require("./validator-tests-ajv-errors-testDefs"); - } - - var gpii = fluid.registerNamespace("gpii"); - - jqUnit.module("Testing validator static functions."); - - jqUnit.test("Testing `gpii.schema.deriveRequiredProperties`.", function () { - jqUnit.assertDeepEq("We should be able to generate a list of required sub-properties.", ["foo", "bar"], gpii.schema.deriveRequiredProperties({ - foo: {required: true}, - bar: {required: true}, - baz: {} - })); - jqUnit.assertDeepEq("We should be able to handle an empty object.", [], gpii.schema.deriveRequiredProperties({})); - }); - - jqUnit.test("Testing `gpii.schema.gssToJsonSchema`.", function () { - var testDefs = { - allSorts: { - message: "We should be able to handle a full range of values.", - gssSchema: {properties: {foo: {enum: [[0, 1, 2], true, "a string", undefined]}}}, - expected: { - "$schema": "http://json-schema.org/draft-07/schema#", - properties: {foo: {enum: [[0, 1, 2], true, "a string", undefined]}} - } - }, - anyOf: { - message: "We should be able to handle array constructs like `anyOf`.", - gssSchema: {anyOf: [{properties: {foo: {required: true}}}, {properties: {errors: {required: true}}}]}, - expected: { - "$schema": "http://json-schema.org/draft-07/schema#", anyOf: [ - {required: ["foo"], properties: {foo: {}}}, - {required: ["errors"], properties: {errors: {}}} - ] - } - }, - emptySchema: { - message: "We should be able to handle an empty schema.", - gssSchema: {}, - expected: {"$schema": "http://json-schema.org/draft-07/schema#"} - }, - enumLabels: { - message: "We should be able to handle our custom `enumLabels` element.", - gssSchema: {properties: {foo: {enumLabels: ["foo"], enum: ["bar"]}}}, - expected: {"$schema": "http://json-schema.org/draft-07/schema#", properties: {foo: {enum: ["bar"]}}} - }, - hint: { - message: "We should be able to handle our custom `hint` element.", - gssSchema: {properties: {foo: {hint: "baz"}}}, - expected: {"$schema": "http://json-schema.org/draft-07/schema#", properties: {foo: {}}} - }, - errors: { - message: "We should be able to handle our custom `errors` element.", - gssSchema: {properties: {foo: {errors: {"": "baz"}}}}, - expected: {"$schema": "http://json-schema.org/draft-07/schema#", properties: {foo: {}}} - }, - specialPropertyNames: { - message: "We should not filter out properties that match our GSS keywords.", - gssSchema: {properties: {errors: {}, hint: {}}}, - expected: {"$schema": "http://json-schema.org/draft-07/schema#", properties: {errors: {}, hint: {}}} - }, - requiredField: { - message: "We should be able to handle a single required field.", - gssSchema: {properties: {foo: {required: true}}}, - expected: {"$schema": "http://json-schema.org/draft-07/schema#", required: ["foo"], properties: {foo: {}}} - }, - requiredFields: { - message: "We should be able to handle multiple required fields.", - gssSchema: {properties: {foo: {required: true}, bar: {required: true, type: "string"}}}, - expected: { - "$schema": "http://json-schema.org/draft-07/schema#", - required: ["foo", "bar"], - properties: {foo: {}, bar: {type: "string"}} - } - } - }; - - fluid.each(testDefs, function (testDef) { - jqUnit.assertDeepEq(testDef.message, testDef.expected, gpii.schema.gssToJsonSchema(testDef.gssSchema)); - }); - }); - - jqUnit.test("Testing `gpii.schema.removeEmptyItems`.", function () { - var testDefs = { - nonEmpty: {message: "Non-empty items should be preserved.", input: ["a", "string"], expected: ["a", "string"]}, - emptyStrings: { - message: "Empty strings should be removed.", - input: ["", "not empty", ""], - expected: ["not empty"] - }, - numbers: {message: "Numbers should be allowed.", input: [1, 2, 3], expected: [1, 2, 3]}, - undefined: { - message: "Undefined elements should be removed.", - input: [1, undefined, 2, undefined, 3], - expected: [1, 2, 3] - }, - null: {message: "Null elements should be removed.", input: [1, null, 2, null, 3], expected: [1, 2, 3]}, - invalidItems: { - message: "Elements other than strings or integers should be removed.", - input: [true, Math.PI, {}, []], - expected: [] - } - }; - fluid.each(testDefs, function (testDef) { - jqUnit.assertDeepEq(testDef.message, testDef.expected, testDef.input.filter(gpii.schema.removeEmptyItems)); - }); - }); - - jqUnit.test("Testing `gpii.schema.validator.errorHintForRule`.", function () { - var testDefs = { - defaultMessage: { - message: "We should be able to failover to a default message.", - gssSchema: {newRule: "shiny"}, - rulePath: ["newRule"], - defaultMessage: "This is the default message.", - expected: "This is the default message." - }, - ruleMessage: { - message: "We should be able to failover to one of the built-in keys for a given rule.", - gssSchema: {type: "text"}, - rulePath: ["type"], - defaultMessage: "This is the default message.", - expected: "gpii.schema.messages.validationErrors.type" - }, - shortForm: { - message: "We should be able to use a 'short form' error definition.", - gssSchema: {type: "text", "errors": "short-form-key"}, - rulePath: ["type"], - defaultMessage: "This is the default message.", - expected: "short-form-key" - }, - longFormByRule: { - message: "We should be able to use a 'long form' error definition for a single rule.", - gssSchema: {type: "text", "errors": {type: "long-form-by-rule-key"}}, - rulePath: ["type"], - defaultMessage: "This is the default message.", - expected: "long-form-by-rule-key" - }, - longFormFailover: { - message: "We should be able to use a 'long form' error definition for multiple rules.", - gssSchema: {type: "text", "errors": {"": "long-form-failover-key"}}, - rulePath: ["type"], - defaultMessage: "This is the default message.", - expected: "long-form-failover-key" - } - }; - fluid.each(testDefs, function (testDef) { - jqUnit.assertEquals(testDef.message, testDef.expected, gpii.schema.validator.errorHintForRule(testDef.rulePath, testDef.gssSchema, testDef.defaultMessage)); - }); - }); - - jqUnit.test("Testing `gpii.schema.validator.extractElDataPathSegmentsFromError`.", function () { - var testDefs = { - rootDataPath: { - message: "We should be able to handle the root dataPath.", - error: {dataPath: ""}, - expected: [] - }, - deepPath: { - message: "We should be able to handle a deep dataPath.", - error: {dataPath: ".deep.path.to.material"}, - expected: ["deep", "path", "to", "material"] - }, - requiredTopLevelField: { - message: "We should be able to handle a missing top-level required field.", - error: {dataPath: "", keyword: "required", params: {missingProperty: "requiredField"}}, - expected: ["requiredField"] - }, - requiredDeepField: { - message: "We should be able to handle a missing deep required field.", - error: {dataPath: ".deep", keyword: "required", params: {missingProperty: "requiredField"}}, - expected: ["deep", "requiredField"] - }, - dottedField: { - message: "We should be able to handle a dotted field in a dataPath.", - error: {"dataPath": "['dotted.field']"}, - expected: ["dotted.field"] - }, - dottedRequiredField: { - message: "We should be able to handle a missing required field with a dot in its key.", - error: { - "keyword": "required", - "dataPath": "", - "params": { - "missingProperty": "dotted.field" - } - }, - expected: ["dotted.field"] - }, - apostropheField: { - message: "We should be able to handle a dataPath with an escaped apostrophe", - error: { - "dataPath": "['don\\'t']" - }, - expected: ["don't"] - }, - complexMixture: { - message: "We should be able to handle complex interleaved data paths.", - error: { - "dataPath": ".settingsHandlers['configure'].supportedSettings['MagnificationMode'].schema" - }, - expected: ["settingsHandlers", "configure", "supportedSettings", "MagnificationMode", "schema"] - } - }; - fluid.each(testDefs, function (testDef) { - jqUnit.assertDeepEq(testDef.message, testDef.expected, gpii.schema.validator.extractElDataPathSegmentsFromError(testDef.error)); - }); - }); - - jqUnit.test("Testing `gpii.schema.validator.jsonPointerToElPath`.", function () { - var testDefs = { - withId: { - message: "We should be able to handle a pointer that includes an ID.", - input: "schema-id.json#/path/to/material", - expected: ["path", "to", "material"] - }, - relative: { - message: "We should be able to handle a relative pointer.", - input: "#/path/to/material", - expected: ["path", "to", "material"] - }, - arrayEntry: { - message: "We should be able to handle a pointer that contains a reference to an array element.", - input: "#/anyOf/1/type", - expected: ["anyOf", "1", "type"] - }, - root: { message: "We should be able to handle a pointer to the root of the object.", input: "#/", expected: [] } - }; - fluid.each(testDefs, function (testDef) { - jqUnit.assertDeepEq(testDef.message, testDef.expected, gpii.schema.validator.jsonPointerToElPath(testDef.input)); - }); - }); - - jqUnit.test("Testing `gpii.schema.validator.standardiseAjvErrors`.", function () { - fluid.each(gpii.tests.validator.ajvErrors, function (testDef) { - var standardisedErrors = gpii.schema.validator.standardiseAjvErrors(testDef.schema, testDef.input); - jqUnit.assertDeepEq(testDef.message, testDef.expected, standardisedErrors); - }); - - var expected = {isValid: true, errors: []}; - var noErrorOutput = gpii.schema.validator.standardiseAjvErrors({}, false); - jqUnit.assertDeepEq("We should be able to handle the case in which there are no AJV errors.", expected, noErrorOutput); - }); - - jqUnit.test("Testing `gpii.schema.validator.localiseErrors`.", function () { - var testDefs = { - defaultsNoData: { - message: "We should be able to resolve error message keys with only the defaults.", - errors: [{message: "gpii.schema.messages.validationErrors.required"}], - expected: [{message: "This value is required."}] - }, - defaultData: { - message: "We should be able to resolve error message keys with the defaults and the data being validated.", - toValidate: true, - errors: [{message: "gpii.schema.messages.validationErrors.required", dataPath: [""]}], - expected: [{message: "This value is required.", dataPath: [""]}] - }, - noErrors: { - message: "We should be able to handle the case in which there are no errors.", - errors: [], - expected: [] - }, - ruleInTemplate: { - message: "We should be able to reference information from the rule in an error template.", - errors: [{ - "dataPath": [], - "schemaPath": [ - "maxLength" - ], - "rule": { - "type": "string", - "maxLength": 2 - }, - "message": "gpii.schema.messages.validationErrors.maxLength" - }], - expected: [{ - "dataPath": [], - "schemaPath": [ - "maxLength" - ], - "rule": { - "type": "string", - "maxLength": 2 - }, - "message": "The value must be 2 characters or less long." - }] - }, - customMessageBundle: { - message: "We should be able to use a custom message bundle.", - messages: { - "gpii.schema.messages.validationErrors.maxLength": "The value is too long." - }, - errors: [{ - "message": "gpii.schema.messages.validationErrors.maxLength" - }], - expected: [{ - "message": "The value is too long." - }] - }, - dataInTemplate: { - message: "We should be able to use data in a custom error template.", - toValidate: {value: "the value itself"}, - messages: {"custom-template": "We have nothing to evaluate but %data.value."}, - errors: [{ - "dataPath": [], - "message": "custom-template" - }], - expected: [{ - "dataPath": [], - "message": "We have nothing to evaluate but the value itself." - }] - }, - rootValue: { - message: "We should be able to use a root data value.", - toValidate: "root", - messages: {"custom-template": "We have discovered the %data of the problem."}, - errors: [{ - "dataPath": [], - "message": "custom-template" - }], - expected: [{ - "dataPath": [], - "message": "We have discovered the root of the problem." - }] - }, - customTransform: { - message: "We should be able to supply our own localisation rules.", - toValidate: {old: "change"}, - messages: {"custom-template": "The only constant is %data.new."}, - localisationTransform: {data: {"new": "data.old"}}, - errors: [{ - "dataPath": [], - "message": "custom-template" - }], - expected: [{ - "dataPath": [], - "message": "The only constant is change." - }] - } - }; - fluid.each(testDefs, function (testDef) { - var output = gpii.schema.validator.localiseErrors(testDef.errors, testDef.toValidate, testDef.messages, testDef.localisationTransform); - jqUnit.assertDeepEq(testDef.message, testDef.expected, output); - }); - }); -})(fluid, jqUnit); diff --git a/tests/testem.js b/tests/testem.js index 9343900..df66e28 100644 --- a/tests/testem.js +++ b/tests/testem.js @@ -23,7 +23,8 @@ var testemComponent = gpii.testem.instrumentation({ "node_modules": "%gpii-json-schema/node_modules" }, additionalProxies: { - hbs: "/hbs", + templates: "/templates", + messages: "/messages", gated: "/gated" }, testemOptions: { @@ -56,11 +57,25 @@ var testemComponent = gpii.testem.instrumentation({ inline: { type: "gpii.handlebars.inlineTemplateBundlingMiddleware", options: { - path: "/hbs", + path: "/templates", priority: "after:urlencoded", templateDirs: ["%gpii-json-schema/src/templates", "%gpii-json-schema/tests/templates"] } }, + messageLoader: { + type: "gpii.handlebars.i18n.messageLoader", + options: { + messageDirs: { validation: "%gpii-json-schema/src/messages" } + } + }, + messages: { + type: "gpii.handlebars.inlineMessageBundlingMiddleware", + options: { + model: { + messageBundles: "{messageLoader}.model.messageBundles" + } + } + }, handlebars: { type: "gpii.express.hb", options: { From 092ae40b4de170525b682fe69125af1e4eabb53b Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Mon, 30 Sep 2019 15:18:28 +0200 Subject: [PATCH 17/27] GPII-4022: Finished error binder "resource loading" refactor. --- src/js/client/errorBinder.js | 38 ++--- .../browser-fixtures/js/errorBinder-tests.js | 133 ++++++++++++--- tests/js/node/lib/harness.js | 57 ++++--- tests/testem.js | 151 +++++++++--------- 4 files changed, 243 insertions(+), 136 deletions(-) diff --git a/src/js/client/errorBinder.js b/src/js/client/errorBinder.js index a9fac27..7b15eb1 100644 --- a/src/js/client/errorBinder.js +++ b/src/js/client/errorBinder.js @@ -48,8 +48,7 @@ }, modelListeners: { validationResults: { - func: "{gpii.schema.client.errorBinder}.renderErrors", - excludeSource: "init" + func: "{gpii.schema.client.errorBinder}.renderErrors" } } } @@ -134,25 +133,7 @@ message: false, validationResults: false }, - components: { - // We have to wait to render until the renderer is available, but also reload if our templates change. - gatedModelWatcher: { - options: { - modelListeners: { - validationResults: [ - { - func: "{gpii.schema.client.errorBinder}.renderInitialMarkup", - excludeSource: "init" - }, - { - func: "{gpii.schema.client.errorBinder}.renderErrors", - excludeSource: "init" - } - ] - } - } - }, success: { options: { model: { @@ -162,7 +143,7 @@ }, error: { options: { - template: "validation-error-summary", + templateKey: "validation-error-summary", model: { message: "{gpii.schema.client.errorAwareForm}.model.errorMessage", validationResults: "{gpii.schema.client.errorAwareForm}.model.validationResults" @@ -171,6 +152,21 @@ validationResults: "{that}.renderInitialMarkup" } } + }, + // Use the "gated model watcher" defined above to ensure that rerender waits for the renderer. + gatedModelWatcher: { + options: { + modelListeners: { + validationResults: [ + { + func: "{gpii.schema.client.errorBinder}.renderInitialMarkup" + }, + { + func: "{gpii.schema.client.errorBinder}.renderErrors" + } + ] + } + } } }, invokers: { diff --git a/tests/browser-fixtures/js/errorBinder-tests.js b/tests/browser-fixtures/js/errorBinder-tests.js index 77739c1..c8a5427 100644 --- a/tests/browser-fixtures/js/errorBinder-tests.js +++ b/tests/browser-fixtures/js/errorBinder-tests.js @@ -9,7 +9,7 @@ var fluid = fluid || {}; gpii.tests.schema.errorBinder.checkElements = function (elementDefs) { fluid.each(elementDefs, function (elementDef) { var elements = fluid.makeArray($(elementDef.selector)); - jqUnit.assertTrue("There should be " + elementDef.expectedElements + " element(s).", elements.length === elementDef.expectedElements); + jqUnit.assertEquals("We should find the right number of elements.", elementDef.expectedElements, elements.length); fluid.each(elements, function (element) { if (elementDef.mustMatch) { @@ -35,6 +35,48 @@ var fluid = fluid || {}; $(selector).submit(); }; + + fluid.defaults("gpii.tests.schema.errorBinder.startSequenceElement", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { func: "{testEnvironment}.events.constructFixtures.fire" }, + { + event: "{testEnvironment}.events.onFixturesReady", + listener: "fluid.identity" + }, + // Crude additional pause to give the component and all sub-components a chance to (re)render. + { + func: "{testEnvironment}.startPause", + args: [150] // timeToPauseInMs + }, + { + event: "{testEnvironment}.events.onPauseComplete", + listener: "fluid.identity" + } + ] + }); + + // If I leave the teardown and reconstruction to createOnEvent, I end up with errors about not being able to call + // `removeListener`, so I destroy the component myself here. + fluid.defaults("gpii.tests.schema.errorBinder.stopSequenceElement", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [{ func: "{testEnvironment}.errorBinder.destroy" }] + }); + + fluid.defaults("gpii.tests.schema.errorBinder.sequenceGrade", { + gradeNames: ["fluid.test.sequence"], + sequenceElements: { + start: { + priority: "before:sequence", + gradeNames: "gpii.tests.schema.errorBinder.startSequenceElement" + }, + stop: { + priority: "after:sequence", + gradeNames: "gpii.tests.schema.errorBinder.stopSequenceElement" + } + } + }); + fluid.defaults("gpii.tests.schema.errorBinder.caseHolder", { gradeNames: ["fluid.test.testCaseHolder"], modules: [{ @@ -42,12 +84,10 @@ var fluid = fluid || {}; tests: [ { name: "Confirm that initial client-side validation errors appear correctly after startup...", + sequenceGrade: "gpii.tests.schema.errorBinder.sequenceGrade", sequence: [ - // TODO: Convert to sequence grade. - { func: "{testEnvironment}.events.constructFixtures.fire" }, { - event: "{testEnvironment}.events.onFixturesReady", - listener: "gpii.tests.schema.errorBinder.checkElements", + func: "gpii.tests.schema.errorBinder.checkElements", args: [[ // Summary message. { @@ -67,15 +107,20 @@ var fluid = fluid || {}; }, { name: "Confirm that feedback on a required field is set and unset as needed...", + sequenceGrade: "gpii.tests.schema.errorBinder.sequenceGrade", sequence: [ - { func: "{testEnvironment}.events.constructFixtures.fire" }, { - event: "{testEnvironment}.events.onFixturesReady", - listener: "gpii.tests.schema.errorBinder.changeModelValue", + func: "gpii.tests.schema.errorBinder.changeModelValue", args: ["{testEnvironment}", "errorBinder", "shallowlyRequired", "has a value"] // environment, componentName, valuePath, valueToSet }, { - func: "gpii.tests.schema.errorBinder.checkElements", + event: "{testEnvironment}.events.onFixturesReady", + func: "{testEnvironment}.startPause", + args: [1050] // timeToPauseInMs + }, + { + event: "{testEnvironment}.events.onPauseComplete", + listener: "gpii.tests.schema.errorBinder.checkElements", args: [[ // Summary message. { @@ -93,8 +138,14 @@ var fluid = fluid || {}; func: "gpii.tests.schema.errorBinder.changeModelValue", args: ["{testEnvironment}", "errorBinder", "shallowlyRequired", null] // environment, componentName, valuePath, valueToSet }, + // Crude pause to give various components a chance to rerender. { - func: "gpii.tests.schema.errorBinder.checkElements", + func: "{testEnvironment}.startPause", + args: [150] // timeToPauseInMs + }, + { + event: "{testEnvironment}.events.onPauseComplete", + listener: "gpii.tests.schema.errorBinder.checkElements", args: [[ // Summary message. { @@ -114,15 +165,20 @@ var fluid = fluid || {}; }, { name: "Confirm that multiple errors can be set and cleared in real time...", + sequenceGrade: "gpii.tests.schema.errorBinder.sequenceGrade", sequence: [ - { func: "{testEnvironment}.events.constructFixtures.fire" }, { - event: "{testEnvironment}.events.onFixturesReady", - listener: "gpii.tests.schema.errorBinder.changeModelValue", + func: "gpii.tests.schema.errorBinder.changeModelValue", args: ["{testEnvironment}", "errorBinder", "testAllOf", "CAT"] // environment, componentName, valuePath, valueToSet }, + // Crude pause to give various components a chance to rerender. { - func: "gpii.tests.schema.errorBinder.checkElements", + func: "{testEnvironment}.startPause", + args: [150] // timeToPauseInMs + }, + { + event: "{testEnvironment}.events.onPauseComplete", + listener: "gpii.tests.schema.errorBinder.checkElements", args: [[ // Summary message. { @@ -145,8 +201,14 @@ var fluid = fluid || {}; func: "gpii.tests.schema.errorBinder.changeModelValue", args: ["{testEnvironment}", "errorBinder", "shallowlyRequired", "There is now text."] // environment, componentName, valuePath, valueToSet }, + // Crude pause to give various components a chance to rerender. { - func: "gpii.tests.schema.errorBinder.checkElements", + func: "{testEnvironment}.startPause", + args: [150] // timeToPauseInMs + }, + { + event: "{testEnvironment}.events.onPauseComplete", + listener: "gpii.tests.schema.errorBinder.checkElements", args: [[ // Summary message. { @@ -164,16 +226,21 @@ var fluid = fluid || {}; }, { name: "Confirm that form submission is prevented if there are validation errors...", + sequenceGrade: "gpii.tests.schema.errorBinder.sequenceGrade", sequence: [ - { func: "{testEnvironment}.events.constructFixtures.fire" }, { - event: "{testEnvironment}.events.onFixturesReady", - listener: "gpii.tests.schema.errorBinder.submitForm", + func: "gpii.tests.schema.errorBinder.submitForm", args: [".errorBinder-viewport form"] // selector }, // If the page were reloaded by a form submit, we would not exist to ever finish this run. + // Crude pause to give various components a chance to rerender. { - func: "gpii.tests.schema.errorBinder.checkElements", + func: "{testEnvironment}.startPause", + args: [150] // timeToPauseInMs + }, + { + event: "{testEnvironment}.events.onPauseComplete", + listener: "gpii.tests.schema.errorBinder.checkElements", args: [[ // Summary message. { @@ -193,20 +260,31 @@ var fluid = fluid || {}; }, { name: "Confirm that form submission succeeeds if there are no validation errors...", + sequenceGrade: "gpii.tests.schema.errorBinder.sequenceGrade", sequence: [ - { func: "{testEnvironment}.events.constructFixtures.fire" }, { - event: "{testEnvironment}.events.onFixturesReady", - listener: "gpii.tests.schema.errorBinder.changeModelValue", + func: "gpii.tests.schema.errorBinder.changeModelValue", args: ["{testEnvironment}", "errorBinder", "shallowlyRequired", "has a value"] // environment, componentName, valuePath, valueToSet }, + // Crude pause to give various components a chance to rerender. { - func: "gpii.tests.schema.errorBinder.submitForm", + func: "{testEnvironment}.startPause", + args: [150] // timeToPauseInMs + }, + { + event: "{testEnvironment}.events.onPauseComplete", + listener: "gpii.tests.schema.errorBinder.submitForm", args: [".errorBinder-viewport form"] // selector }, // If the page were reloaded by a form submit, we would not exist to ever finish this run. + // Crude pause to give various components a chance to rerender. { event: "{testEnvironment}.errorBinder.events.requestReceived", + listener: "{testEnvironment}.startPause", + args: [150] // timeToPauseInMs + }, + { + event: "{testEnvironment}.events.onPauseComplete", listener: "gpii.tests.schema.errorBinder.checkElements", args: [[ // Success message @@ -238,7 +316,14 @@ var fluid = fluid || {}; markupFixture: ".errorBinder-viewport", events: { constructFixtures: null, - onFixturesReady: null + onFixturesReady: null, + onPauseComplete: null + }, + invokers: { + startPause: { + funcName: "setTimeout", + args: ["{that}.events.onPauseComplete.fire", "{arguments}.0"] // timeToPauseInMs + } }, components: { caseHolder: { diff --git a/tests/js/node/lib/harness.js b/tests/js/node/lib/harness.js index f283c7c..25bc03b 100644 --- a/tests/js/node/lib/harness.js +++ b/tests/js/node/lib/harness.js @@ -14,21 +14,9 @@ fluid.require("%gpii-handlebars"); require("../../../../"); require("./middleware-express-fixtures.js"); -fluid.defaults("gpii.test.schema.harness", { - gradeNames: ["gpii.express"], - port: 6194, - baseUrl: { - expander: { - funcName: "fluid.stringTemplate", - args: ["http://localhost:%port/", { port: "{that}.options.port" }] - } - }, - config: { - express: { - "port" : "{that}.options.port", - baseUrl: "{that}.options.url" - } - }, +fluid.defaults("gpii.test.schema.harness.base", { + gradeNames: ["fluid.component"], + templateDirs: ["%gpii-json-schema/src/templates", "%gpii-json-schema/tests/templates", "%gpii-handlebars/tests/templates/primary"], components: { json: { type: "gpii.express.middleware.bodyparser.json", @@ -46,7 +34,7 @@ fluid.defaults("gpii.test.schema.harness", { type: "gpii.express.hb", options: { priority: "after:urlencoded", - templateDirs: ["%gpii-json-schema/tests/templates", "%gpii-json-schema/src/templates"] + templateDirs: "{gpii.test.schema.harness.base}.options.templateDirs" } }, gated: { @@ -73,7 +61,7 @@ fluid.defaults("gpii.test.schema.harness", { modules: { type: "gpii.express.router.static", options: { - path: "/modules", + path: "/node_modules", content: "%gpii-json-schema/node_modules" } }, @@ -87,8 +75,22 @@ fluid.defaults("gpii.test.schema.harness", { inline: { type: "gpii.handlebars.inlineTemplateBundlingMiddleware", options: { - path: "/hbs", - templateDirs: ["%gpii-json-schema/src/templates", "%gpii-json-schema/tests/templates"] + path: "/templates", + templateDirs: "{gpii.test.schema.harness.base}.options.templateDirs" + } + }, + messageLoader: { + type: "gpii.handlebars.i18n.messageLoader", + options: { + messageDirs: { validation: "%gpii-json-schema/src/messages" } + } + }, + messages: { + type: "gpii.handlebars.inlineMessageBundlingMiddleware", + options: { + model: { + messageBundles: "{messageLoader}.model.messageBundles" + } } }, htmlErrorHandler: { @@ -109,3 +111,20 @@ fluid.defaults("gpii.test.schema.harness", { } } }); + +fluid.defaults("gpii.test.schema.harness", { + gradeNames: ["gpii.express", "gpii.test.schema.harness.base"], + port: 6194, + baseUrl: { + expander: { + funcName: "fluid.stringTemplate", + args: ["http://localhost:%port/", { port: "{that}.options.port" }] + } + } + //config: { + // express: { + // "port" : "{that}.options.port", + // baseUrl: "{that}.options.url" + // } + //}, +}); diff --git a/tests/testem.js b/tests/testem.js index df66e28..0bc75a5 100644 --- a/tests/testem.js +++ b/tests/testem.js @@ -8,6 +8,11 @@ fluid.require("%gpii-json-schema"); fluid.require("%gpii-handlebars"); require("./js/node/lib/middleware-express-fixtures"); +require("./js/node/lib/harness"); + +fluid.defaults("gpii.test.schema.coverageServer", { + gradeNames: ["gpii.testem.coverage.express", "gpii.test.schema.harness.base"] +}); var testemComponent = gpii.testem.instrumentation({ reportsDir: "reports", @@ -22,6 +27,7 @@ var testemComponent = gpii.testem.instrumentation({ "tests": "%gpii-json-schema/tests", "node_modules": "%gpii-json-schema/node_modules" }, + templateDirs: ["%gpii-json-schema/src/templates", "%gpii-json-schema/tests/templates", "%gpii-handlebars/tests/templates/primary"], additionalProxies: { templates: "/templates", messages: "/messages", @@ -34,78 +40,79 @@ var testemComponent = gpii.testem.instrumentation({ }, components: { express: { - options: { - components: { - json: { - type: "gpii.express.middleware.bodyparser.json", - options: { - priority: "first", - middlewareOptions: { - limit: 12500000 // Allow coverage payloads of up to 100Mb instead of the default 100Kb - } - } - }, - urlencoded: { - type: "gpii.express.middleware.bodyparser.urlencoded", - options: { - priority: "after:json", - middlewareOptions: { - limit: 12500000 // Allow coverage payloads of up to 100Mb instead of the default 100Kb - } - } - }, - inline: { - type: "gpii.handlebars.inlineTemplateBundlingMiddleware", - options: { - path: "/templates", - priority: "after:urlencoded", - templateDirs: ["%gpii-json-schema/src/templates", "%gpii-json-schema/tests/templates"] - } - }, - messageLoader: { - type: "gpii.handlebars.i18n.messageLoader", - options: { - messageDirs: { validation: "%gpii-json-schema/src/messages" } - } - }, - messages: { - type: "gpii.handlebars.inlineMessageBundlingMiddleware", - options: { - model: { - messageBundles: "{messageLoader}.model.messageBundles" - } - } - }, - handlebars: { - type: "gpii.express.hb", - options: { - priority: "after:urlencoded", - templateDirs: ["%gpii-json-schema/tests/templates", "%gpii-json-schema/src/templates"] - } - }, - gated: { - type: "gpii.tests.schema.middleware.router", - options: { - priority: "after:urlencoded" - } - }, - htmlErrorHandler: { - type: "gpii.handlebars.errorRenderingMiddleware", - options: { - priority: "after:gated", - statusCode: 400, - templateKey: "validation-error-summary" - } - }, - defaultErrorMiddleware: { - type: "gpii.express.middleware.error", - options: { - priority: "after:htmlErrorHandler", - defaultStatusCode: 400 - } - } - } - } + type: "gpii.test.schema.coverageServer" + // options: { + // components: { + // json: { + // type: "gpii.express.middleware.bodyparser.json", + // options: { + // priority: "first", + // middlewareOptions: { + // limit: 12500000 // Allow coverage payloads of up to 100Mb instead of the default 100Kb + // } + // } + // }, + // urlencoded: { + // type: "gpii.express.middleware.bodyparser.urlencoded", + // options: { + // priority: "after:json", + // middlewareOptions: { + // limit: 12500000 // Allow coverage payloads of up to 100Mb instead of the default 100Kb + // } + // } + // }, + // inline: { + // type: "gpii.handlebars.inlineTemplateBundlingMiddleware", + // options: { + // path: "/templates", + // priority: "after:urlencoded", + // templateDirs: "{gpii.testem.instrumentation}.options.templateDirs" + // } + // }, + // messageLoader: { + // type: "gpii.handlebars.i18n.messageLoader", + // options: { + // messageDirs: { validation: "%gpii-json-schema/src/messages" } + // } + // }, + // messages: { + // type: "gpii.handlebars.inlineMessageBundlingMiddleware", + // options: { + // model: { + // messageBundles: "{messageLoader}.model.messageBundles" + // } + // } + // }, + // handlebars: { + // type: "gpii.express.hb", + // options: { + // priority: "after:urlencoded", + // templateDirs: "{gpii.testem.instrumentation}.options.templateDirs" + // } + // }, + // gated: { + // type: "gpii.tests.schema.middleware.router", + // options: { + // priority: "after:urlencoded" + // } + // }, + // htmlErrorHandler: { + // type: "gpii.handlebars.errorRenderingMiddleware", + // options: { + // priority: "after:gated", + // statusCode: 400, + // templateKey: "validation-error-summary" + // } + // }, + // defaultErrorMiddleware: { + // type: "gpii.express.middleware.error", + // options: { + // priority: "after:htmlErrorHandler", + // defaultStatusCode: 400 + // } + // } + // } + // } } } }); From 0f1e7ad2ed2094fbcaea8f92caff6cd95e13d360 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Thu, 3 Oct 2019 14:36:35 +0200 Subject: [PATCH 18/27] GPII-4022: Updated to favour schema as resource instead of adding our own promise handling. Also fixed browser tests. --- .nycrc | 2 +- package.json | 6 +- src/js/common/schemaHolder.js | 5 +- src/js/server/schemaValidationMiddleware.js | 38 ++++------ tests/browser-fixtures/all-tests.html | 1 + .../browser-fixtures/schema-holder-tests.html | 31 ++++++++ tests/js/node/lib/harness.js | 53 ++++++------- tests/testem.js | 76 +------------------ 8 files changed, 81 insertions(+), 131 deletions(-) create mode 100644 tests/browser-fixtures/schema-holder-tests.html diff --git a/.nycrc b/.nycrc index 5441076..6fa6c64 100644 --- a/.nycrc +++ b/.nycrc @@ -2,5 +2,5 @@ "reporter": ["html", "text-summary"], "report-dir": "reports", "temp-directory": "coverage", - "include": [ "./src/**/*.js", "./index.js"] + "include": [ "src/**/*.js", "index.js"] } diff --git a/package.json b/package.json index ed441c2..4d7b375 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,13 @@ "kettle": "1.11.0" }, "devDependencies": { - "eslint": "6.5.0", + "eslint": "6.5.1", "eslint-config-fluid": "1.3.0", "foundation-sites": "6.4.1", "gpii-grunt-lint-all": "1.0.5", - "gpii-testem": "2.1.10", + "gpii-testem": "2.1.11-dev.20191003T113129Z.477bcc0.GPII-4156", "grunt": "1.0.4", - "handlebars": "4.4.0", + "handlebars": "4.4.2", "markdown-it": "10.0.0", "mkdirp": "0.5.1", "node-jqunit": "1.1.8", diff --git a/src/js/common/schemaHolder.js b/src/js/common/schemaHolder.js index e03e2e1..26c1886 100644 --- a/src/js/common/schemaHolder.js +++ b/src/js/common/schemaHolder.js @@ -6,10 +6,11 @@ */ /* globals require */ +fluid = fluid || require("infusion"); + (function (fluid) { "use strict"; - if (!fluid) { - fluid = require("infusion"); + if (fluid.require) { fluid.require("%gpii-json-schema"); } diff --git a/src/js/server/schemaValidationMiddleware.js b/src/js/server/schemaValidationMiddleware.js index b68bf28..1a74882 100644 --- a/src/js/server/schemaValidationMiddleware.js +++ b/src/js/server/schemaValidationMiddleware.js @@ -30,7 +30,7 @@ fluid.require("%gpii-handlebars"); * * @param {gpii.schema.validator} validatorComponent - The global validator component. * @param {gpii.schema.validationMiddleware} schemaMiddlewareComponent - The middleware component. - * @param {Object|Promise} schema - The GSS schema to validate against, or a promise that will resolve to same. + * @param {Object} schema - The GSS schema to validate against. * @param {Object} req - The Express request object. * @param {Object} res - The Express response object. * @param {Function} next - The function to be executed next in the middleware chain. @@ -39,28 +39,22 @@ fluid.require("%gpii-handlebars"); gpii.schema.validationMiddleware.rejectOrForward = function (validatorComponent, schemaMiddlewareComponent, schema, req, res, next) { var toValidate = fluid.model.transformWithRules(req, schemaMiddlewareComponent.options.rules.requestContentToValidate); - var schemaAsPromise = fluid.isPromise(schema) ? schema : fluid.toPromise(schema); - schemaAsPromise.then( - function (schema) { - var validationResults = validatorComponent.validate(schema, toValidate, schemaMiddlewareComponent.options.schemaHash); + var validationResults = validatorComponent.validate(schema, toValidate, schemaMiddlewareComponent.options.schemaHash); - if (validationResults.isError) { - next(validationResults); - } - else if (validationResults.isValid) { - next(); - } - else { - var messageBundle = gpii.handlebars.i18n.deriveMessageBundleFromRequest(req, schemaMiddlewareComponent.model.messageBundles, schemaMiddlewareComponent.options.defaultLocale); - var localisedErrors = gpii.schema.validator.localiseErrors(validationResults.errors, toValidate, messageBundle, schemaMiddlewareComponent.options.localisationTransform); - var localisedPayload = fluid.copy(validationResults); - localisedPayload.errors = localisedErrors; - localisedPayload.statusCode = schemaMiddlewareComponent.options.invalidStatusCode; - next(localisedPayload); - } - }, - next - ); + if (validationResults.isError) { + next(validationResults); + } + else if (validationResults.isValid) { + next(); + } + else { + var messageBundle = gpii.handlebars.i18n.deriveMessageBundleFromRequest(req, schemaMiddlewareComponent.model.messageBundles, schemaMiddlewareComponent.options.defaultLocale); + var localisedErrors = gpii.schema.validator.localiseErrors(validationResults.errors, toValidate, messageBundle, schemaMiddlewareComponent.options.localisationTransform); + var localisedPayload = fluid.copy(validationResults); + localisedPayload.errors = localisedErrors; + localisedPayload.statusCode = schemaMiddlewareComponent.options.invalidStatusCode; + next(localisedPayload); + } }; /* diff --git a/tests/browser-fixtures/all-tests.html b/tests/browser-fixtures/all-tests.html index 322a6d0..377a1fd 100644 --- a/tests/browser-fixtures/all-tests.html +++ b/tests/browser-fixtures/all-tests.html @@ -15,6 +15,7 @@ "/tests/browser-fixtures/errorBinder-tests.html", "/tests/browser-fixtures/metaschema-tests.html", "/tests/browser-fixtures/orderedStringify-tests.html", + "/tests/browser-fixtures/schema-holder-tests.html", "/tests/browser-fixtures/schema-validated-modelComponent-tests.html", "/tests/browser-fixtures/schema-validated-component-tests.html", "/tests/browser-fixtures/schema-validated-component-pre-potentia-ii-tests.html", diff --git a/tests/browser-fixtures/schema-holder-tests.html b/tests/browser-fixtures/schema-holder-tests.html new file mode 100644 index 0000000..3d86c8f --- /dev/null +++ b/tests/browser-fixtures/schema-holder-tests.html @@ -0,0 +1,31 @@ + + + "Schema Holder" Tests + + + + + + + + + + + + + + + + + + +

"Schema Holder" Tests

+

+
+

+
    + + + + + diff --git a/tests/js/node/lib/harness.js b/tests/js/node/lib/harness.js index 25bc03b..f798e98 100644 --- a/tests/js/node/lib/harness.js +++ b/tests/js/node/lib/harness.js @@ -51,27 +51,6 @@ fluid.defaults("gpii.test.schema.harness.base", { content: "%gpii-json-schema/build" } }, - js: { - type: "gpii.express.router.static", - options: { - path: "/src", - content: "%gpii-json-schema/src" - } - }, - modules: { - type: "gpii.express.router.static", - options: { - path: "/node_modules", - content: "%gpii-json-schema/node_modules" - } - }, - content: { - type: "gpii.express.router.static", - options: { - path: "/content", - content: "%gpii-json-schema/tests/browser-fixtures" - } - }, inline: { type: "gpii.handlebars.inlineTemplateBundlingMiddleware", options: { @@ -120,11 +99,27 @@ fluid.defaults("gpii.test.schema.harness", { funcName: "fluid.stringTemplate", args: ["http://localhost:%port/", { port: "{that}.options.port" }] } - } - //config: { - // express: { - // "port" : "{that}.options.port", - // baseUrl: "{that}.options.url" - // } - //}, -}); + }, + components: { + js: { + type: "gpii.express.router.static", + options: { + path: "/src", + content: "%gpii-json-schema/src" + } + }, + modules: { + type: "gpii.express.router.static", + options: { + path: "/node_modules", + content: "%gpii-json-schema/node_modules" + } + }, + content: { + type: "gpii.express.router.static", + options: { + path: "/content", + content: "%gpii-json-schema/tests/browser-fixtures" + } + } + }}); diff --git a/tests/testem.js b/tests/testem.js index 0bc75a5..bf49342 100644 --- a/tests/testem.js +++ b/tests/testem.js @@ -15,8 +15,8 @@ fluid.defaults("gpii.test.schema.coverageServer", { }); var testemComponent = gpii.testem.instrumentation({ - reportsDir: "reports", - coverageDir: "coverage", + reportsDir: "%gpii-json-schema/reports", + coverageDir: "%gpii-json-schema/coverage", testPages: [ "tests/browser-fixtures/all-tests.html" ], @@ -41,78 +41,6 @@ var testemComponent = gpii.testem.instrumentation({ components: { express: { type: "gpii.test.schema.coverageServer" - // options: { - // components: { - // json: { - // type: "gpii.express.middleware.bodyparser.json", - // options: { - // priority: "first", - // middlewareOptions: { - // limit: 12500000 // Allow coverage payloads of up to 100Mb instead of the default 100Kb - // } - // } - // }, - // urlencoded: { - // type: "gpii.express.middleware.bodyparser.urlencoded", - // options: { - // priority: "after:json", - // middlewareOptions: { - // limit: 12500000 // Allow coverage payloads of up to 100Mb instead of the default 100Kb - // } - // } - // }, - // inline: { - // type: "gpii.handlebars.inlineTemplateBundlingMiddleware", - // options: { - // path: "/templates", - // priority: "after:urlencoded", - // templateDirs: "{gpii.testem.instrumentation}.options.templateDirs" - // } - // }, - // messageLoader: { - // type: "gpii.handlebars.i18n.messageLoader", - // options: { - // messageDirs: { validation: "%gpii-json-schema/src/messages" } - // } - // }, - // messages: { - // type: "gpii.handlebars.inlineMessageBundlingMiddleware", - // options: { - // model: { - // messageBundles: "{messageLoader}.model.messageBundles" - // } - // } - // }, - // handlebars: { - // type: "gpii.express.hb", - // options: { - // priority: "after:urlencoded", - // templateDirs: "{gpii.testem.instrumentation}.options.templateDirs" - // } - // }, - // gated: { - // type: "gpii.tests.schema.middleware.router", - // options: { - // priority: "after:urlencoded" - // } - // }, - // htmlErrorHandler: { - // type: "gpii.handlebars.errorRenderingMiddleware", - // options: { - // priority: "after:gated", - // statusCode: 400, - // templateKey: "validation-error-summary" - // } - // }, - // defaultErrorMiddleware: { - // type: "gpii.express.middleware.error", - // options: { - // priority: "after:htmlErrorHandler", - // defaultStatusCode: 400 - // } - // } - // } - // } } } }); From bfa3dad8b6bffedbfc0198b5dc7f887e9071d0bc Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Fri, 4 Oct 2019 09:35:18 +0200 Subject: [PATCH 19/27] GPII-4022: Relaxed "events" definition for schema-validated components to allow unresolved IoC references in potentia-ii versions of Infusion. --- src/js/common/schemaValidatedComponent.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/js/common/schemaValidatedComponent.js b/src/js/common/schemaValidatedComponent.js index 68d0652..f284ab9 100644 --- a/src/js/common/schemaValidatedComponent.js +++ b/src/js/common/schemaValidatedComponent.js @@ -206,6 +206,10 @@ var fluid = fluid || require("infusion"); "type": "array" } } + }, + // Required to allow IoC references, discuss hardening further. + { + type: "string" } ] } From c85ce4bc63644c45fd04f3e416ccbc28052cd203 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Fri, 4 Oct 2019 09:57:44 +0200 Subject: [PATCH 20/27] GPII-4022: Removed wrong-headed "tabindex" from validation errors. --- src/templates/partials/validation-error-inline.handlebars | 4 ++-- src/templates/partials/validation-error-summary.handlebars | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/templates/partials/validation-error-inline.handlebars b/src/templates/partials/validation-error-inline.handlebars index c660b86..dd351b4 100644 --- a/src/templates/partials/validation-error-inline.handlebars +++ b/src/templates/partials/validation-error-inline.handlebars @@ -1,3 +1,3 @@ -
    +
    {{messageHelper message}} -
    \ No newline at end of file +
    diff --git a/src/templates/partials/validation-error-summary.handlebars b/src/templates/partials/validation-error-summary.handlebars index bf79dfa..2ad0d98 100644 --- a/src/templates/partials/validation-error-summary.handlebars +++ b/src/templates/partials/validation-error-summary.handlebars @@ -2,7 +2,7 @@
    {{message}}
    {{/if}} {{#if validationResults.errors}} -
    +

    The information you provided is incomplete or incorrect. Please check the following:

      @@ -11,4 +11,4 @@ {{/each}}
    -{{/if}} \ No newline at end of file +{{/if}} From 63301b8b627143a2c37abd7756671cb1c48d31c2 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Fri, 4 Oct 2019 10:24:46 +0200 Subject: [PATCH 21/27] GPII-4022: Removed overly-agressive rerender when validation errors are encountered. --- src/js/client/errorBinder.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/js/client/errorBinder.js b/src/js/client/errorBinder.js index 7b15eb1..c2d2914 100644 --- a/src/js/client/errorBinder.js +++ b/src/js/client/errorBinder.js @@ -158,9 +158,6 @@ options: { modelListeners: { validationResults: [ - { - func: "{gpii.schema.client.errorBinder}.renderInitialMarkup" - }, { func: "{gpii.schema.client.errorBinder}.renderErrors" } From 08f4f4f5887f52d8a9be5a215f455ff824065b8c Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Fri, 4 Oct 2019 12:16:52 +0200 Subject: [PATCH 22/27] GPII-4022: Updated to fix failure to render on startup in downstream grades. --- src/js/client/errorBinder.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/js/client/errorBinder.js b/src/js/client/errorBinder.js index c2d2914..ea85ea1 100644 --- a/src/js/client/errorBinder.js +++ b/src/js/client/errorBinder.js @@ -173,9 +173,13 @@ } }, listeners: { + // Break the contract inherited from gpii-handlebars. "onCreate.renderMarkup": { funcName: "fluid.identity" }, + "onRendererAvailable.renderMarkup": { + func: "{that}.renderInitialMarkup" + }, "onResourcesLoaded.log": { funcName: "console.log", args: ["Resources loaded..."] From 2d0aae36405cee5702f297d58715b053157bec2a Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Fri, 11 Oct 2019 10:03:55 +0200 Subject: [PATCH 23/27] GPII-4022: Updated to use latest gpii-handlebars dev release with refactored message resolver. Updated linting config and fixed errors. --- docs/validator.md | 4 ++-- package.json | 10 +++++----- tests/browser-fixtures/errorBinder-demo.html | 1 + tests/browser-fixtures/errorBinder-tests.html | 14 ++++++++------ tests/js/common/lib/generate-sample-ajv-errors.js | 3 ++- tests/js/node/lib/harness.js | 6 +++--- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/docs/validator.md b/docs/validator.md index 17b2635..b3d4257 100644 --- a/docs/validator.md +++ b/docs/validator.md @@ -218,7 +218,7 @@ fluid.defaults("my.validating.component", { var myValidatingComponent = my.validating.component(); var validationResults = myValidatingComponent.validateInput({}); -console.log(JSON.stringify(validationResults, null, 2)); +fluid.log(JSON.stringify(validationResults, null, 2)); /* { "isValid": false, @@ -243,7 +243,7 @@ console.log(JSON.stringify(validationResults, null, 2)); */ var secondValidationResults = myValidatingComponent.validateInput({ foo: true}); -console.log(JSON.stringify(secondValidationResults, null, 2)); +fluid.log(JSON.stringify(secondValidationResults, null, 2)); /* { isValid: true diff --git a/package.json b/package.json index 4d7b375..6eaa71d 100644 --- a/package.json +++ b/package.json @@ -18,20 +18,20 @@ "license": "BSD-3-Clause", "dependencies": { "ajv": "6.10.2", - "gpii-binder": "1.0.5", + "gpii-binder": "1.0.6", "gpii-express": "1.0.15", - "gpii-handlebars": "2.1.0-dev.20190923T144705Z.38550ff.GPII-4100", - "infusion": "3.0.0-dev.20190926T213421Z.e84b3cf0e.FLUID-6148", + "gpii-handlebars": "2.1.0-dev.20191011T075156Z.b698e3b.GPII-4100", + "infusion": "3.0.0-dev.20191009T141140Z.32c9263b4.FLUID-6148", "kettle": "1.11.0" }, "devDependencies": { "eslint": "6.5.1", - "eslint-config-fluid": "1.3.0", + "eslint-config-fluid": "1.4.0", "foundation-sites": "6.4.1", "gpii-grunt-lint-all": "1.0.5", "gpii-testem": "2.1.11-dev.20191003T113129Z.477bcc0.GPII-4156", "grunt": "1.0.4", - "handlebars": "4.4.2", + "handlebars": "4.4.3", "markdown-it": "10.0.0", "mkdirp": "0.5.1", "node-jqunit": "1.1.8", diff --git a/tests/browser-fixtures/errorBinder-demo.html b/tests/browser-fixtures/errorBinder-demo.html index ae88ff8..475ee92 100644 --- a/tests/browser-fixtures/errorBinder-demo.html +++ b/tests/browser-fixtures/errorBinder-demo.html @@ -25,6 +25,7 @@ + diff --git a/tests/browser-fixtures/errorBinder-tests.html b/tests/browser-fixtures/errorBinder-tests.html index daeef99..fdcf4a4 100644 --- a/tests/browser-fixtures/errorBinder-tests.html +++ b/tests/browser-fixtures/errorBinder-tests.html @@ -21,16 +21,19 @@ - - - - + + - + + + + + + @@ -38,7 +41,6 @@ - diff --git a/tests/js/common/lib/generate-sample-ajv-errors.js b/tests/js/common/lib/generate-sample-ajv-errors.js index 60199c5..77f98ac 100644 --- a/tests/js/common/lib/generate-sample-ajv-errors.js +++ b/tests/js/common/lib/generate-sample-ajv-errors.js @@ -126,4 +126,5 @@ fluid.each(gpii.tests.validator.ajvErrors, function (testDef, key) { filteredErrorDefs[key] = fluid.filterKeys(testDef, ["input", "schema"], true); }); -console.log(JSON.stringify(fluid.merge({}, filteredErrorDefs, combinedResults), null, 2)); +fluid.setLogging(true); +fluid.log(JSON.stringify(fluid.merge({}, filteredErrorDefs, combinedResults), null, 2)); diff --git a/tests/js/node/lib/harness.js b/tests/js/node/lib/harness.js index f798e98..c25e62d 100644 --- a/tests/js/node/lib/harness.js +++ b/tests/js/node/lib/harness.js @@ -58,8 +58,8 @@ fluid.defaults("gpii.test.schema.harness.base", { templateDirs: "{gpii.test.schema.harness.base}.options.templateDirs" } }, - messageLoader: { - type: "gpii.handlebars.i18n.messageLoader", + messageBundleLoader: { + type: "gpii.handlebars.i18n.messageBundleLoader", options: { messageDirs: { validation: "%gpii-json-schema/src/messages" } } @@ -68,7 +68,7 @@ fluid.defaults("gpii.test.schema.harness.base", { type: "gpii.handlebars.inlineMessageBundlingMiddleware", options: { model: { - messageBundles: "{messageLoader}.model.messageBundles" + messageBundles: "{messageBundleLoader}.model.messageBundles" } } }, From 68a1b550b21ea2d9d3ada8dafc5b949a1659c5f4 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Fri, 11 Oct 2019 16:28:07 +0200 Subject: [PATCH 24/27] GPII-4022: Updated to latest gpii-handlebars to pick up "prioritsable template directory" work. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6eaa71d..0baf9e5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "ajv": "6.10.2", "gpii-binder": "1.0.6", "gpii-express": "1.0.15", - "gpii-handlebars": "2.1.0-dev.20191011T075156Z.b698e3b.GPII-4100", + "gpii-handlebars": "2.1.0-dev.20191011T135302Z.1fce29e.GPII-4100", "infusion": "3.0.0-dev.20191009T141140Z.32c9263b4.FLUID-6148", "kettle": "1.11.0" }, From bd209d8a7834ae0466355df47960a9956e7409be Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Mon, 14 Oct 2019 15:24:12 +0200 Subject: [PATCH 25/27] GPII-4022: Updated lingering template directory options arrays. --- package.json | 2 +- tests/js/node/lib/harness.js | 6 +++++- tests/testem.js | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0baf9e5..65f840e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "ajv": "6.10.2", "gpii-binder": "1.0.6", "gpii-express": "1.0.15", - "gpii-handlebars": "2.1.0-dev.20191011T135302Z.1fce29e.GPII-4100", + "gpii-handlebars": "2.1.0-dev.20191014T131654Z.fd29700.GPII-4100", "infusion": "3.0.0-dev.20191009T141140Z.32c9263b4.FLUID-6148", "kettle": "1.11.0" }, diff --git a/tests/js/node/lib/harness.js b/tests/js/node/lib/harness.js index c25e62d..648d6b1 100644 --- a/tests/js/node/lib/harness.js +++ b/tests/js/node/lib/harness.js @@ -16,7 +16,11 @@ require("./middleware-express-fixtures.js"); fluid.defaults("gpii.test.schema.harness.base", { gradeNames: ["fluid.component"], - templateDirs: ["%gpii-json-schema/src/templates", "%gpii-json-schema/tests/templates", "%gpii-handlebars/tests/templates/primary"], + templateDirs: { + validation: "%gpii-json-schema/src/templates", + validationTests: "%gpii-json-schema/tests/templates", + handlebarsTests: "%gpii-handlebars/tests/templates/primary" + }, components: { json: { type: "gpii.express.middleware.bodyparser.json", diff --git a/tests/testem.js b/tests/testem.js index bf49342..8fa47e7 100644 --- a/tests/testem.js +++ b/tests/testem.js @@ -27,7 +27,11 @@ var testemComponent = gpii.testem.instrumentation({ "tests": "%gpii-json-schema/tests", "node_modules": "%gpii-json-schema/node_modules" }, - templateDirs: ["%gpii-json-schema/src/templates", "%gpii-json-schema/tests/templates", "%gpii-handlebars/tests/templates/primary"], + templateDirs: { + validation: "%gpii-json-schema/src/templates", + validationTests: "%gpii-json-schema/tests/templates", + handlebarsTests: "%gpii-handlebars/tests/templates/primary" + }, additionalProxies: { templates: "/templates", messages: "/messages", From 8a372f7cf72fd0047db58380c83d7b0d61bde817 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Mon, 14 Oct 2019 16:33:06 +0200 Subject: [PATCH 26/27] GPII-4022: Brought in newer version of gpii-handlebars with improved directory prioritisation. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 65f840e..9f6effe 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "ajv": "6.10.2", "gpii-binder": "1.0.6", "gpii-express": "1.0.15", - "gpii-handlebars": "2.1.0-dev.20191014T131654Z.fd29700.GPII-4100", + "gpii-handlebars": "2.1.0-dev.20191014T141924Z.45a74ef.GPII-4100", "infusion": "3.0.0-dev.20191009T141140Z.32c9263b4.FLUID-6148", "kettle": "1.11.0" }, From 1393ce9ce3a248f44c640f08b8e7c14702da0e73 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Wed, 16 Oct 2019 15:59:57 +0200 Subject: [PATCH 27/27] GPII-4022: temporary fix for windows Firefox test timeouts. --- tests/testem.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/testem.js b/tests/testem.js index 8fa47e7..13c52a0 100644 --- a/tests/testem.js +++ b/tests/testem.js @@ -37,6 +37,14 @@ var testemComponent = gpii.testem.instrumentation({ messages: "/messages", gated: "/gated" }, + // Force Firefox to run headless as a temporary fix for Firefox issues on Windows: + // https://github.com/testem/testem/issues/1377 + "browserArgs": { + "Firefox": [ + "--no-remote", + "--headless" + ] + }, testemOptions: { // Disable Headless Chrome we can figure out a solution to this issue: https://issues.gpii.net/browse/GPII-4064 // Running Testem with the HEADLESS environment variable still works, and still runs headless.