diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..a2e796f --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,22 @@ +name: JS Commons Documentation +on: + release: + types: [published] +jobs: + deploy: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [14.x] + steps: + - uses: actions/setup-node@v1 + - uses: actions/checkout@v2 + - run: npm install + - run: npm run docs + - uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/@openeo/js-commons + keep_files: true + user_name: 'openEO CI' + user_email: openeo.ci@uni-muenster.de \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6c146bc --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,17 @@ +name: JS Commons Tests +on: [push, pull_request] +jobs: + deploy: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [14.x] + steps: + - uses: actions/setup-node@v1 + - uses: actions/checkout@v2 + - run: npm install + - run: npm run lint + - run: npm run build + - run: npm run docs + - run: npm run test + - run: npm run test_node \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cad0d42..0000000 --- a/.travis.yml +++ /dev/null @@ -1,36 +0,0 @@ -language: node_js - -node_js: - - "stable" - -sudo: false - -cache: - directories: - - "node_modules" - -script: -# Run code checks - - npm run compat -# Run tests - - npm run test - - npm run test_node -# Build - - npm run build -# Generate docs - - npm run docs -# Deploy - - git clone --branch gh-pages https://$GITHUB_TOKEN@github.com/Open-EO/openeo-js-commons.git gh-pages - - cd gh-pages - - cp -R ../docs/@openeo/js-commons/* . - -deploy: - provider: pages - skip-cleanup: true - github-token: $GITHUB_TOKEN - keep-history: true - name: openEO CI - email: openeo.ci@uni-muenster.de - local-dir: gh-pages - on: - all_branches: true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ab3c091 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.3.0] - 2021-07-05 + +### Added + +- New method in `Utils`: `hasText` +- New classes (migrated over from the Web Editor): + - `ProcessParameter` + - `ProcessSchema` + - `ProcessDataType` + +### Changed + +- `npm run compat` was renamed to `npm run lint` + +## [1.2.0] - 2020-09-03 + +### Added + +- Support specifying a `keyPath` in `ProcessUtils.getCallbackParameters*()` methods to support `load_collection` filters. + +## [1.1.1] - 2020-08-13 + +### Fixed + +- `Utils.unique` did not work on arrays of objects + +## [1.1.0] - 2020-08-13 + +### Added + +- New methods in `Utils`: + - `equals` + - `mapObject` + - `mapObjectValues` + - `omitFromObject` + - `pickFromObject` + - `unique` +- New class `ProcessUtils` + +## [1.0.0] - 2020-07-21 + +First release supporting openEO API 1.0.0. + +## Prior releases + +All prior releases have been documented in the [GitHub Releases](https://github.com/Open-EO/openeo-js-commons/releases). + +[Unreleased]: +[1.3.0]: +[1.2.0]: +[1.1.1]: +[1.1.0]: +[1.0.0]: diff --git a/README.md b/README.md index 3b3a0a7..7b26ae9 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ A set of common JavaScript functionalities for [openEO](http://openeo.org). -The [master branch](https://github.com/Open-EO/openeo-api/tree/master) is the 'stable' version of library, which is currently version **1.2.0**. +The [master branch](https://github.com/Open-EO/openeo-api/tree/master) is the 'stable' version of library, which is currently version **1.3.0**. The [draft branch](https://github.com/Open-EO/openeo-api/tree/draft) is where active development takes place. -[![Build Status](https://travis-ci.org/Open-EO/openeo-js-commons.svg?branch=master)](https://travis-ci.org/Open-EO/openeo-js-commons) ![Dependencies](https://img.shields.io/librariesio/release/npm/@openeo/js-commons) -![Minified Size](https://img.shields.io/bundlephobia/min/@openeo/js-commons/1.2.0) -![Minzipped Size](https://img.shields.io/bundlephobia/minzip/@openeo/js-commons/1.2.0) +![Minified Size](https://img.shields.io/bundlephobia/min/@openeo/js-commons/1.3.0) +![Minzipped Size](https://img.shields.io/bundlephobia/minzip/@openeo/js-commons/1.3.0) ![Supported API Versions](https://img.shields.io/github/package-json/apiVersions/Open-Eo/openeo-js-commons/master) +![JS Commons Tests](https://github.com/Open-EO/openeo-js-commons/workflows/JS%20Commons%20Tests/badge.svg) ## Features - Converting responses from API version 0.4 to the latest API version is supported for: @@ -40,4 +40,4 @@ In a web environment you can include the library as follows: ``` -More information can be found in the [**JS commons documentation**](https://open-eo.github.io/openeo-js-commons/1.2.0/). \ No newline at end of file +More information can be found in the [**JS commons documentation**](https://open-eo.github.io/openeo-js-commons/1.3.0/). \ No newline at end of file diff --git a/package.json b/package.json index de1a789..e687c25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openeo/js-commons", - "version": "1.2.0", + "version": "1.3.0", "apiVersions": [ "0.4.x", "1.0.x" @@ -44,7 +44,7 @@ "scripts": { "docs": "jsdoc src -r -d docs/ -P package.json -R README.md", "build": "npx webpack", - "compat": "jshint src", + "lint": "jshint src", "test": "jest --env=jsdom", "test_node": "jest --env=node" } diff --git a/src/main.js b/src/main.js index ae18e6a..624a708 100644 --- a/src/main.js +++ b/src/main.js @@ -2,8 +2,12 @@ const MigrateCapabilities = require('./migrate/capabilities'); const MigrateCollections = require('./migrate/collections'); const MigrateProcesses = require('./migrate/processes'); -// Others +// Processes +const ProcessDataType = require('./processDataType'); +const ProcessParameter = require('./processParameter'); +const ProcessSchema = require('./processSchema'); const ProcessUtils = require('./processUtils'); +// Others const Versions = require('./versions'); const Utils = require('./utils'); @@ -11,6 +15,9 @@ module.exports = { MigrateCapabilities, MigrateCollections, MigrateProcesses, + ProcessDataType, + ProcessParameter, + ProcessSchema, ProcessUtils, Versions, Utils, diff --git a/src/processDataType.js b/src/processDataType.js new file mode 100644 index 0000000..8a21413 --- /dev/null +++ b/src/processDataType.js @@ -0,0 +1,204 @@ +const Utils = require('./utils'); + +/** + * Wrapper class for a single data type definition in a schema (e.g. process parameter schema, return value schema). + * + * @class + */ +class ProcessDataType { + + /** + * Constructs a new process data type based on JSON Schema. + * + * @param {object} schema + * @param {?ProcessSchema} [parent=null] + * @param {*} [defaultValue=undefined] + */ + constructor(schema, parent = null, defaultValue = undefined) { + this.schema = schema; + if (typeof this.schema.default === 'undefined') { + this.schema.default = defaultValue; + } + this.parent = parent; + } + + /** + * Converts the schema to a JSON-serializable representation. + * + * @returns {object} + */ + toJSON() { + return Object.assign({}, this.schema, {default: this.default()}); + } + + /** + * Checks whether the data type is only `null`. + * + * @returns {boolean} + */ + isAny() { + return this.dataType() === 'any'; + } + + /** + * Checks whether the data type is only `null`. + * + * @returns {boolean} + */ + isNull() { + return this.schema.type === 'null'; + } + + /** + * Checks whether the data type allows `null`. + * + * @returns {boolean} + */ + nullable() { + return this.isNull() || this.isAny(); + } + + /** + * Returns whether the data type is editable. + * + * This means it returns `true`, unless certain data types are detected that + * can't be transmitted via JSON in the openEO API (e.g. data cubes or labeled arrays). + * + * @returns {boolean} + */ + isEditable() { + return !ProcessDataType.NON_EDITABLE.includes(this.dataType()); + } + + /** + * Returns the data type. + * + * The priority is as such: + * - subtype + * - native data type + * - "any" + * + * @param {boolean} [native=false] - Set to true to only return the native data type. + * @returns {string} + */ + dataType(native = false) { + let nativeType = this.schema.type || "any"; + return native ? nativeType : (this.schema.subtype || nativeType); + } + + /** + * Returns the native data type of the schema. + * + * One of: array, object, null, string, boolean, number or any + * + * @returns {string} + */ + nativeDataType() { + return this.dataType(true); + } + + /** + * Checks whether the data type contains an enumeration of values. + * + * @returns {boolean} + * @see ProcessDataType#getEnumChoices + */ + isEnum() { + return Array.isArray(this.schema.enum) && this.schema.enum.length > 0; + } + + /** + * Returns the allowed enumeration of values. + * + * @returns {array} + * @see ProcessDataType#isEnum + */ + getEnumChoices() { + return this.isEnum() ? this.schema.enum : []; + } + + /** + * Returns the parameters for a "child process" that is defined for the data type. + * + * @returns {array} + */ + getCallbackParameters() { + return Array.isArray(this.schema.parameters) ? this.schema.parameters : []; + } + + /** + * Returns the group of the data type. + * + * Group is a "extension" of JSON Schema, which allows to group schemas by certain criteria. + * + * @returns {string} + */ + group() { + return Utils.hasText(this.schema.group) ? this.schema.group : ProcessDataType.DEFAULT_GROUP; + } + + /** + * Returns the title of the data type. + * + * If no title is present, returns a "prettified" version of the data type + * (e.g. "Temporal Interval" for the data type "temporal-interval"). + * + * @returns {string} + */ + title() { + if (Utils.hasText(this.schema.title)) { + return this.schema.title; + } + else { + return Utils.prettifyString(this.dataType()); + } + } + + /** + * Returns the description of the data type. + * + * @returns {string} + */ + description() { + return Utils.hasText(this.schema.description) ? this.schema.description : ""; + } + + /** + * Returns the default value of the data type. + * + * This may return `undefined`. + * + * @returns {*} + */ + default() { + if (typeof this.schema.default === 'function') { + return this.schema.default(); + } + return this.schema.default; + } + +} + +/** + * The name of the default group for schemas. + * + * Defaults to `Other`. + * + * @type {string} + */ +ProcessDataType.DEFAULT_GROUP = 'Other'; +/** + * A list of data types that can't be edited. + * + * Non-editable data types can't be transmitted via JSON through the openEO API + * (e.g. data cubes or labeled arrays). + * + * @type {array} + */ +ProcessDataType.NON_EDITABLE = [ + 'raster-cube', + 'vector-cube', + 'labeled-array' +]; + +module.exports = ProcessDataType; \ No newline at end of file diff --git a/src/processParameter.js b/src/processParameter.js new file mode 100644 index 0000000..e84f6a6 --- /dev/null +++ b/src/processParameter.js @@ -0,0 +1,23 @@ +const ProcessSchema = require('./processSchema'); + +/** + * Wrapper class for a process parameter. + * + * @class + */ +class ProcessParameter extends ProcessSchema { + + /** + * Constructs a new process parameter based on the openEO API representation. + * + * @param {object} parameter + */ + constructor(parameter) { + super(parameter.schema, parameter.default); + + Object.assign(this, parameter); + } + +} + +module.exports = ProcessParameter; \ No newline at end of file diff --git a/src/processSchema.js b/src/processSchema.js new file mode 100644 index 0000000..3a264e2 --- /dev/null +++ b/src/processSchema.js @@ -0,0 +1,154 @@ +const ProcessUtils = require('./processUtils'); +const ProcessDataType = require('./processDataType'); +const Utils = require('./utils'); + +/** + * Wrapper class for the process schemas (i.e. from parameters or return value). + * + * @class + */ +class ProcessSchema { + + /** + * Constructs a new process schema based on the openEO API representation. + * + * Can be array or JSON Schema object. The array consists of multiple JSON Schemas then. + * + * @param {?object|array} [schema=null] + * @param {*} [defaultValue=undefined] + */ + constructor(schema = null, defaultValue = undefined) { + if (!Utils.isObject(schema) && !Array.isArray(schema)) { + this.unspecified = true; + this.schemas = []; + } + else { + this.unspecified = false; + this.schemas = ProcessUtils.normalizeJsonSchema(schema, true).map(s => new ProcessDataType(s, this, defaultValue)); + + // Find and assign the default value from sub-schemas if no defaultValue was given + if (typeof defaultValue === 'undefined') { + let defaults = this.schemas + .map(s => s.default()) + .filter(d => typeof d !== 'undefined'); + this.default = defaults[0]; + } + else { + this.default = defaultValue; + } + } + + this.refs = []; + } + + /** + * Converts the schemas to a JSON-serializable representation. + * + * @returns {object} + */ + toJSON() { + return this.schemas.map(s => s.toJSON()); + } + + /** + * Returns whether the schema is editable. + * + * This means it returns `true`, unless certain data types are detected that + * can't be transmitted via JSON in the openEO API (e.g. data cubes or labeled arrays). + * + * @returns {boolean} + */ + isEditable() { + return (this.unspecified || this.schemas.filter(s => s.isEditable() && !s.isNull()).length > 0); + } + + /** + * Checks whether the schema is exactly and only of the given data type. + * + * Can be a native type or a openEO "subtype". + * + * @param {string} type + * @returns {boolean} + */ + is(type) { + var types = this.dataTypes(); + return (types.length === 1 && types[0] === type); + } + + /** + * Returns the native data type of the schema. + * + * One of: array, object, null, string, boolean, number + * + * @returns {string} + */ + nativeDataType() { + return this.dataType(true); + } + + /** + * Returns the data type of the associated schemas. + * + * Setting `native` to `true` will only consider native JSON data types and "any". + * Otherwise, subtypes will also be considered. + * + * If the schema has a two data types and one of them is `null`, + * `null` is ignored and just the other data type is returned. + * + * `nullable()` can be used to check whether a schema allows `null`. + * + * Returns `mixed` if multiple data types are allowed. + * + * @param {boolean} [native=false] + * @returns {string} + * @see ProcessSchema#nullable + */ + dataType(native = false) { + var types = this.dataTypes(true, native); + var nullIndex = types.indexOf('null'); + if (types.length === 1) { + return types[0]; + } + else if (types.length === 2 && nullIndex !== -1) { + return types[nullIndex === 0 ? 1 : 0]; + } + else { + return 'mixed'; + } + } + + /** + * Returns a set of all supported distinct data types (or 'any'). + * + * By default, `null` is not included in the list of data types. + * Setting `includeNull` to `true` to include `null` in the list. + * + * Setting `native` to `true` will only consider native JSON data types and "any". + * Otherwise, subtypes will also be considered. + * + * @param {boolean} [includeNull=false] + * @param {boolean} [native=false] + * @returns {array} + */ + dataTypes(includeNull = false, native = false) { + var types = this.schemas + .map(s => s.dataType(native)) + .filter((v, i, a) => a.indexOf(v) === i); // Return each type only once + if (types.length === 0 || types.includes('any')) { + return ['any']; + } + return includeNull ? types : types.filter(s => s !== 'null'); + } + + /** + * Checks whether one of the schemas allows the value to be `null`. + * + * @returns {boolean} + */ + nullable() { + return (this.unspecified || this.schemas.filter(s => s.nullable()).length > 0); + } + +} + +module.exports = ProcessSchema; \ No newline at end of file diff --git a/src/processUtils.js b/src/processUtils.js index c7b00e6..c9e5891 100644 --- a/src/processUtils.js +++ b/src/processUtils.js @@ -1,4 +1,4 @@ -const Utils = require('./utils.js'); +const Utils = require('./utils'); /** * Utilities to parse process specs and JSON schemas. diff --git a/src/utils.js b/src/utils.js index 29874cc..cee9b74 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,5 @@ var equal = require('fast-deep-equal/es6'); - /** * General utilities * @@ -21,6 +20,16 @@ class Utils { return (typeof obj === 'object' && obj === Object(obj) && !Array.isArray(obj)); } + /** + * Checks whether a variable is a string and contains at least one character. + * + * @param {*} string - A variable to check. + * @returns {boolean} - `true` is the given variable is an string with length > 0, `false` otherwise. + */ + static hasText(string) { + return (typeof string === 'string' && string.length > 0); + } + /** * Performs a deep comparison between two values to determine if they are equivalent. * diff --git a/tests/processes.test.js b/tests/processes.test.js new file mode 100644 index 0000000..ea58d55 --- /dev/null +++ b/tests/processes.test.js @@ -0,0 +1,245 @@ +const ProcessParameter = require('../src/processParameter'); +const ProcessSchema = require('../src/processSchema'); +const ProcessDataType = require('../src/processDataType'); +const { array } = require('yargs'); + +describe('Process(Parameter|Schema|DataType) Tests', () => { + + test('ProcessParameter', () => { + let parameter = { + name: 'example', + optional: true, + default: "2020-01-01", + schema: { + type: 'string', + subtype: 'date' + } + }; + + let obj = new ProcessParameter(parameter); + expect(obj.name).toBe(parameter.name); + expect(obj.optional).toBe(parameter.optional); + expect(obj.default).toBe(parameter.default); + expect(obj.schema).toEqual(parameter.schema); + + expect(obj.toJSON()).toEqual([parameter.schema]); + expect(obj.isEditable()).toBe(true); + expect(obj.is('date')).toBe(true); + expect(obj.nativeDataType()).toBe('string'); + expect(obj.dataType()).toBe('date'); + expect(obj.dataTypes()).toEqual(['date']); + expect(obj.nullable()).toBe(false); + + }); + + test('ProcessSchema (empty)', () => { + let obj = new ProcessSchema(); + + expect(obj.unspecified).toBe(true); + expect(obj.schemas).toEqual([]); + expect(obj.default).toBeUndefined(); + + expect(obj.toJSON()).toEqual([]); + expect(obj.isEditable()).toBe(true); + expect(obj.is('null')).toBe(false); + expect(obj.nativeDataType()).toBe('any'); + expect(obj.dataType()).toBe('any'); + expect(obj.dataTypes()).toEqual(['any']); + expect(obj.nullable()).toBe(true); + + }); + + test('ProcessSchema (array)', () => { + let schemas = [ + { + type: 'string', + subtype: 'date-time' + }, + { + type: 'string', + subtype: 'date' + }, + { + type: 'number', + default: 0 + } + ]; + + let obj = new ProcessSchema(schemas); + expect(obj.unspecified).toBe(false); + expect(obj.default).toBe(0); + + expect(obj.toJSON()).toEqual(schemas); + expect(obj.isEditable()).toBe(true); + expect(obj.is('date-time')).toBe(false); + expect(obj.is('date')).toBe(false); + expect(obj.nativeDataType()).toBe('mixed'); + expect(obj.dataType()).toBe('mixed'); + expect(obj.dataTypes()).toEqual(['date-time', 'date', 'number']); + expect(obj.dataTypes(false, true)).toEqual(['string', 'number']); + expect(obj.nullable()).toBe(false); + }); + + test('ProcessSchema (object)', () => { + let defaultValue = null; + let schemas = { + type: ['number', 'null'] + }; + let normalizedSchema = [ + { + type: 'number', + default: defaultValue + }, + { + type: 'null', + default: defaultValue + } + ]; + + let obj = new ProcessSchema(schemas, defaultValue); + expect(obj.unspecified).toBe(false); + expect(obj.default).toBe(defaultValue); + + expect(obj.toJSON()).toEqual(normalizedSchema); + expect(obj.isEditable()).toBe(true); + expect(obj.is('number')).toBe(true); + expect(obj.is('null')).toBe(false); + expect(obj.nativeDataType()).toBe('number'); + expect(obj.dataType()).toBe('number'); + expect(obj.dataTypes(true)).toEqual(['number', 'null']); + expect(obj.dataTypes()).toEqual(['number']); + expect(obj.nullable()).toBe(true); + }); + + test('ProcessSchema (ignore schema default)', () => { + let schema = { + type: 'boolean', + default: false + }; + + let obj = new ProcessSchema(schema, true); + expect(obj.default).toBe(true); + }); + + test('ProcessDataType (enums)', () => { + let type = 'string'; + let title = 'abc'; + let description = 'A, B or C?'; + let _enum = [ + 'A', + 'B', + 'C' + ]; + let schema = { + type, + title, + description, + enum: _enum + }; + + let obj = new ProcessDataType(schema); + + expect(ProcessDataType.DEFAULT_GROUP).toBe('Other'); + + expect(obj.schema).toEqual(schema); + + expect(obj.toJSON()).toEqual(schema); + expect(obj.isAny()).toBe(false); + expect(obj.isNull()).toBe(false); + expect(obj.nullable()).toBe(false); + expect(obj.isEditable()).toBe(true); + expect(obj.dataType()).toBe(type); + expect(obj.nativeDataType()).toBe(type); + expect(obj.isEnum()).toBe(true); + expect(obj.getEnumChoices()).toEqual(_enum); + expect(obj.getCallbackParameters()).toEqual([]); + expect(obj.group()).toBe(ProcessDataType.DEFAULT_GROUP); + expect(obj.title()).toBe(title); + expect(obj.description()).toBe(description); + expect(obj.default()).toBeUndefined(); + }); + + test('ProcessDataType (process-graph)', () => { + let type = 'object'; + let subtype = 'process-graph'; + let group = 'openEO'; + let parameters = [ + { + name: 'x', + description: 'A value.', + schema: { + type: 'number' + } + } + ]; + let _default = null; + let schema = { + type, + subtype, + group, + parameters, + default: _default + }; + + let obj = new ProcessDataType(schema); + expect(obj.schema).toEqual(schema); + + expect(obj.toJSON()).toEqual(schema); + expect(obj.isAny()).toBe(false); + expect(obj.isNull()).toBe(false); + expect(obj.nullable()).toBe(false); + expect(obj.isEditable()).toBe(true); + expect(obj.dataType()).toBe(subtype); + expect(obj.nativeDataType()).toBe(type); + expect(obj.isEnum()).toBe(false); + expect(obj.getEnumChoices()).toEqual([]); + expect(obj.getCallbackParameters()).toEqual(parameters); + expect(obj.group()).toBe(group); + expect(obj.title()).toBe("Process Graph"); + expect(obj.description()).toBe(""); + expect(obj.default()).toBe(_default); + }); + + test('ProcessDataType (any)', () => { + let obj = new ProcessDataType({}, null, {}); + expect(obj.toJSON()).toEqual({ + default: {} + }); + expect(obj.isAny()).toBe(true); + expect(obj.isNull()).toBe(false); + expect(obj.nullable()).toBe(true); + expect(obj.dataType()).toBe('any'); + expect(obj.nativeDataType()).toBe('any'); + expect(obj.default()).toEqual({}); + expect(obj.title()).toBe("Any"); + }); + + test('ProcessDataType (raster-cube)', () => { + let type = 'object'; + let subtype = 'raster-cube'; + let schema = { + type, + subtype, + default() { + return {} + } + }; + + let obj = new ProcessDataType(schema); + expect(obj.schema).toEqual(schema); + + expect(obj.toJSON()).toEqual({ + type, + subtype, + default: {} + }); + expect(obj.isAny()).toBe(false); + expect(obj.isNull()).toBe(false); + expect(obj.nullable()).toBe(false); + expect(obj.isEditable()).toBe(false); + expect(obj.dataType()).toBe(subtype); + expect(obj.default()).toEqual({}); + expect(obj.nativeDataType()).toBe(type); + }); + +}); \ No newline at end of file diff --git a/tests/utils.test.js b/tests/utils.test.js index 609fd97..a7928a6 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -21,6 +21,13 @@ describe('Utils Tests', () => { expect(Utils.size([])).toBe(0); expect(Utils.size([1,2,3])).toBe(3); }); + test('hasText', () => { + expect(Utils.hasText("")).toBe(false); + expect(Utils.hasText(null)).toBe(false); + expect(Utils.hasText(123)).toBe(false); + expect(Utils.hasText("123")).toBe(true); + expect(Utils.hasText(" ")).toBe(true); + }); test('isNumeric', () => { expect(Utils.isNumeric(null)).toBe(false);