From 0c494fe2ad68d02d9fac0b65a1267ee0ebaf4874 Mon Sep 17 00:00:00 2001 From: Gervwyk Date: Tue, 1 Feb 2022 17:32:52 +0200 Subject: [PATCH 01/21] feat: Add start:dev-docs start script. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 2646355af..5f361335a 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "start:server-dev": "yarn workspace @lowdefy/server-dev start --package-manager yarn --config-directory ../../app", "start": "yarn workspace @lowdefy/server build:lowdefy --config-directory ../../app && yarn && yarn workspace @lowdefy/server build:next && yarn workspace @lowdefy/server start", "start:dev": "yarn workspace @lowdefy/server build:lowdefy --config-directory ../../app && yarn && yarn workspace @lowdefy/server dev", + "start:dev-docs": "yarn workspace @lowdefy/server build:lowdefy --config-directory ../docs && yarn && yarn workspace @lowdefy/server dev", "test": "lerna run test", "test:ci": "yarn install --immutable --immutable-cache --check-cache && yarn build && yarn test --ignore='@lowdefy/format' --ignore='@lowdefy/block-dev'" }, From d37db362801de9f0e4d9640fc77e431edeca757a Mon Sep 17 00:00:00 2001 From: Gervwyk Date: Mon, 7 Feb 2022 08:48:54 +0200 Subject: [PATCH 02/21] feat: Add build scripts for plugings. --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index bdd4a7467..0e270b1bd 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,10 @@ "scripts": { "build": "lerna run build", "build:dev": "NODE_ENV=development lerna run build", + "build:plugins": "yarn build:blocks && yarn build:connections && yarn build:operators", + "build:blocks": "lerna run --scope '@lowdefy/blocks-*' build", + "build:connections": "lerna run --scope '@lowdefy/connection-*' build", + "build:operators": "lerna run --scope '@lowdefy/operators-*' build", "clean": "lerna run clean", "lerna:version": "lerna version --no-git-tag-version", "lerna:publish": "lerna publish from-git", From e3f3ad269fbc45ce1dcdc5becc1ed43ff961349e Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 7 Feb 2022 14:00:47 +0200 Subject: [PATCH 03/21] chore(docs): Fix docs test config. --- .pnp.cjs | 2 ++ packages/docs/jest.config.js | 5 +++++ packages/docs/package.json | 2 ++ yarn.lock | 2 ++ 4 files changed, 11 insertions(+) diff --git a/.pnp.cjs b/.pnp.cjs index 22a5f5b1f..9710b0259 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -3214,6 +3214,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./packages/docs/", "packageDependencies": [ ["@lowdefy/docs", "workspace:packages/docs"], + ["@swc/core", "npm:1.2.135"], + ["@swc/jest", "virtual:babee6e81435a5d101529cd67f2c6b175f4db37a4ab0b58df15adf73dd11be8917ac14caf44ab4e6882a92c61661055072365b349016e85173e049f006fc2305#npm:0.2.17"], ["jest", "virtual:babee6e81435a5d101529cd67f2c6b175f4db37a4ab0b58df15adf73dd11be8917ac14caf44ab4e6882a92c61661055072365b349016e85173e049f006fc2305#npm:27.4.7"] ], "linkType": "SOFT", diff --git a/packages/docs/jest.config.js b/packages/docs/jest.config.js index 18cf6f76a..45999c2b3 100644 --- a/packages/docs/jest.config.js +++ b/packages/docs/jest.config.js @@ -7,6 +7,7 @@ export default { '/.lowdefy/', '/jest.config.js', '/coverage/', + '/howto/', ], coverageReporters: [['lcov', { projectRoot: '../..' }], 'text', 'clover'], errorOnDeprecated: true, @@ -15,5 +16,9 @@ export default { '/.lowdefy/', '/jest.config.js', '/coverage/', + '/howto/', ], + transform: { + '^.+\\.(t|j)sx?$': ['@swc/jest', { configFile: '../../.swcrc.test' }], + }, }; diff --git a/packages/docs/package.json b/packages/docs/package.json index 09787bc86..a65dbf4a5 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -32,6 +32,8 @@ "test": "jest --coverage" }, "devDependencies": { + "@swc/core": "1.2.135", + "@swc/jest": "0.2.17", "jest": "27.4.7" }, "publishConfig": { diff --git a/yarn.lock b/yarn.lock index 36df58a5a..9cf3b35c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2347,6 +2347,8 @@ __metadata: version: 0.0.0-use.local resolution: "@lowdefy/docs@workspace:packages/docs" dependencies: + "@swc/core": 1.2.135 + "@swc/jest": 0.2.17 jest: 27.4.7 languageName: unknown linkType: soft From aa1d72c19122eb7d4343108ba6ad21c423dc2493 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 7 Feb 2022 14:01:24 +0200 Subject: [PATCH 04/21] fix: Add missing api and helpers tests. --- .../api/src/context/readConfigFile.test.js | 33 ++++++++++++ packages/utils/helpers/src/cachedPromises.js | 8 +-- .../utils/helpers/src/cachedPromises.test.js | 52 +++++++++++++++++++ 3 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 packages/api/src/context/readConfigFile.test.js create mode 100644 packages/utils/helpers/src/cachedPromises.test.js diff --git a/packages/api/src/context/readConfigFile.test.js b/packages/api/src/context/readConfigFile.test.js new file mode 100644 index 000000000..26db8d884 --- /dev/null +++ b/packages/api/src/context/readConfigFile.test.js @@ -0,0 +1,33 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { getFileExtension } from '@lowdefy/node-utils'; + +jest.mock('@lowdefy/node-utils', () => { + return { + getFileExtension, + readFile: jest.fn(), + }; +}); + +test('readConfigFile', async () => { + const nodeUtils = await import('@lowdefy/node-utils'); + nodeUtils.mockImplementation(() => Promise.resove('config value')); + const createReadConfigFile = (await import('./readConfigFile.js')).default; + const readConfigFile = createReadConfigFile({ buildDirectory: '/build' }); + const res = await readConfigFile('file'); + expect(res).toEqual('config value'); +}); diff --git a/packages/utils/helpers/src/cachedPromises.js b/packages/utils/helpers/src/cachedPromises.js index 63de5868d..1a56d46b6 100644 --- a/packages/utils/helpers/src/cachedPromises.js +++ b/packages/utils/helpers/src/cachedPromises.js @@ -14,10 +14,10 @@ limitations under the License. */ -function cachedPromises(getter) { +function createCachedPromises(getter) { const cache = new Map(); - function getCachedPromise(key) { + function cachedPromises(key) { if (cache.has(key)) { return Promise.resolve(cache.get(key)); } @@ -26,7 +26,7 @@ function cachedPromises(getter) { return Promise.resolve(promise); } - return getCachedPromise; + return cachedPromises; } -export default cachedPromises; +export default createCachedPromises; diff --git a/packages/utils/helpers/src/cachedPromises.test.js b/packages/utils/helpers/src/cachedPromises.test.js new file mode 100644 index 000000000..38d44243e --- /dev/null +++ b/packages/utils/helpers/src/cachedPromises.test.js @@ -0,0 +1,52 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import createCachedPromises from './cachedPromises.js'; +import wait from './wait.js'; + +test('cachedPromises calls getter with key', async () => { + const getter = jest.fn(() => Promise.resolve('value')); + const cachedGetter = createCachedPromises(getter); + const promise = cachedGetter('key'); + expect(`${promise}`).toEqual('[object Promise]'); + const result = await promise; + expect(result).toEqual('value'); + expect(getter.mock.calls).toEqual([['key']]); +}); + +test('cachedPromises only calls getter once', async () => { + const getter = jest.fn(() => Promise.resolve('value')); + const cachedGetter = createCachedPromises(getter); + const result1 = await cachedGetter('key'); + expect(result1).toEqual('value'); + const result2 = await cachedGetter('key'); + expect(result2).toEqual('value'); + expect(getter.mock.calls).toEqual([['key']]); +}); + +test('getter is called once if first call has not yet resolved', async () => { + const getter = jest.fn(async () => { + await wait(10); + return 'value'; + }); + const cachedGetter = createCachedPromises(getter); + const promise1 = cachedGetter('key'); + const promise2 = cachedGetter('key'); + expect(getter.mock.calls).toEqual([['key']]); + await promise1; + await promise2; + expect(getter.mock.calls).toEqual([['key']]); +}); From c774f3f31fcbdf16aabd99d1cf76714cfa4b109f Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 7 Feb 2022 14:18:52 +0200 Subject: [PATCH 05/21] fix(operators-js): Fix getter operator tests. --- packages/operators/jest.config.js | 4 +- .../src/operators/client/_index.test.js | 19 +- .../src/operators/client/actions.test.js | 29 ++- .../src/operators/client/event.test.js | 21 +- .../src/operators/client/event_log.test.js | 239 ++---------------- .../src/operators/client/global.test.js | 28 +- .../src/operators/client/input.test.js | 21 +- .../src/operators/client/location.js | 1 + .../src/operators/client/location.test.js | 7 +- .../operators/client/request_details.test.js | 207 +++------------ .../src/operators/client/state.test.js | 21 +- .../src/operators/client/url_query.test.js | 24 +- .../src/operators/server/payload.test.js | 19 +- .../src/operators/server/secret.test.js | 17 +- .../src/operators/shared/args.test.js | 14 +- .../src/operators/shared/get.test.js | 16 +- .../src/operators/shared/user.test.js | 15 +- 17 files changed, 194 insertions(+), 508 deletions(-) diff --git a/packages/operators/jest.config.js b/packages/operators/jest.config.js index 485aa48d4..40ed50f35 100644 --- a/packages/operators/jest.config.js +++ b/packages/operators/jest.config.js @@ -3,11 +3,11 @@ export default { collectCoverage: true, collectCoverageFrom: ['src/**/*.js'], coverageDirectory: 'coverage', - coveragePathIgnorePatterns: ['/dist/', '/test/', '/dist/index.js'], + coveragePathIgnorePatterns: ['/dist/', '/test/', '/src/index.js'], coverageReporters: [['lcov', { projectRoot: '../..' }], 'text', 'clover'], errorOnDeprecated: true, testEnvironment: 'jsdom', - testPathIgnorePatterns: ['/dist/'], + testPathIgnorePatterns: ['/dist/', '/src/index.js'], transform: { '^.+\\.(t|j)sx?$': ['@swc/jest', { configFile: '../../.swcrc.test' }], }, diff --git a/packages/plugins/operators/operators-js/src/operators/client/_index.test.js b/packages/plugins/operators/operators-js/src/operators/client/_index.test.js index 1384538b1..ed115b485 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/_index.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/_index.test.js @@ -15,17 +15,18 @@ */ import _index from './_index.js'; -jest.mock('@lowdefy/operators'); -const input = { - arrayIndices: [0], - location: 'location', - params: 'params', -}; +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); -test('args calls getFromObject', () => { - const lowdefyOperators = import('@lowdefy/operators'); - _index(input); +test('_index calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); + _index({ + arrayIndices: [0], + location: 'location', + params: 'params', + }); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ { diff --git a/packages/plugins/operators/operators-js/src/operators/client/actions.test.js b/packages/plugins/operators/operators-js/src/operators/client/actions.test.js index 874a98def..3044d1a12 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/actions.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/actions.test.js @@ -15,22 +15,23 @@ */ import actions from './actions.js'; -jest.mock('@lowdefy/operators'); -const input = { - actions: { - action_id: { - response: 'returned from action', +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); + +test('actions calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); + actions({ + actions: { + action_id: { + response: 'returned from action', + }, }, - }, - arrayIndices: [0], - location: 'location', - params: 'params', -}; - -test('actions calls getFromObject', () => { - const lowdefyOperators = import('@lowdefy/operators'); - actions(input); + arrayIndices: [0], + location: 'location', + params: 'params', + }); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ { diff --git a/packages/plugins/operators/operators-js/src/operators/client/event.test.js b/packages/plugins/operators/operators-js/src/operators/client/event.test.js index f9ef4ebba..8f77fbbf6 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/event.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/event.test.js @@ -15,18 +15,19 @@ */ import event from './event.js'; -jest.mock('@lowdefy/operators'); -const input = { - arrayIndices: [0], - event: { event: true }, - location: 'location', - params: 'params', -}; +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); -test('event calls getFromObject', () => { - const lowdefyOperators = import('@lowdefy/operators'); - event(input); +test('event calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); + event({ + arrayIndices: [0], + event: { event: true }, + location: 'location', + params: 'params', + }); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ { diff --git a/packages/plugins/operators/operators-js/src/operators/client/event_log.test.js b/packages/plugins/operators/operators-js/src/operators/client/event_log.test.js index d395fc96a..2831a729d 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/event_log.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/event_log.test.js @@ -14,226 +14,29 @@ limitations under the License. */ -import { WebParser } from '@lowdefy/operators'; +import event_log from './event_log.js'; -const arrayIndices = [1]; +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); -const context = { - _internal: { - lowdefy: { - inputs: { id: true }, - lowdefyGlobal: { global: true }, - menus: [{ menus: true }], - urlQuery: { urlQuery: true }, - user: { user: true }, - }, - }, - eventLog: [{ eventLog: true }], - id: 'id', - requests: [{ requests: true }], - state: { state: true }, -}; - -console.error = () => {}; - -test('_event_log in array', async () => { - const input = { a: { _event_log: '1.blockId' } }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual({ - a: 'block_b', +test('request_details calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); + event_log({ + eventLog: [{ eventName: 'test' }], + arrayIndices: [0], + location: 'location', + params: 'params', }); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_event_log full state', async () => { - const input = { _event_log: true }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual([ - { - blockId: 'block_a', - actionName: 'name_a', - response: [{ data: ['a', 'b'] }], - ts: new Date(0), - status: 'success', - }, - { - blockId: 'block_b', - actionName: 'name_b', - ts: new Date(1), - error: [{ error: 'error', message: 'broken', name: 'e' }], - }, + expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ + [ + { + arrayIndices: [0], + location: 'location', + object: [{ eventName: 'test' }], + operator: '_event_log', + params: 'params', + }, + ], ]); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_event_log null', async () => { - const input = { _event_log: null }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _event_log params must be of type string, integer, boolean or object. Received: null at locationId.], - ] - `); -}); - -test('_event_log param object key', async () => { - const input = { - _event_log: { - key: '0.actionName', - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual('name_a'); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_event_log param object all', async () => { - const input = { - _event_log: { - all: true, - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual([ - { - blockId: 'block_a', - actionName: 'name_a', - response: [{ data: ['a', 'b'] }], - ts: new Date(0), - status: 'success', - }, - { - blockId: 'block_b', - actionName: 'name_b', - ts: new Date(1), - error: [{ error: 'error', message: 'broken', name: 'e' }], - }, - ]); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_event_log param object all and key', async () => { - const input = { - _event_log: { - all: true, - key: 'string', - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual([ - { - blockId: 'block_a', - actionName: 'name_a', - response: [{ data: ['a', 'b'] }], - ts: new Date(0), - status: 'success', - }, - { - blockId: 'block_b', - actionName: 'name_b', - ts: new Date(1), - error: [{ error: 'error', message: 'broken', name: 'e' }], - }, - ]); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_event_log param object invalid', async () => { - const input = { - _event_log: { - other: true, - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _event_log.key must be of type string or integer. Received: {"other":true} at locationId.], - ] - `); -}); - -test('_event_log param array', async () => { - const input = { - _event_log: ['string'], - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _event_log params must be of type string, integer, boolean or object. Received: ["string"] at locationId.], - ] - `); -}); - -test('_event_log param object with string default', async () => { - const input = { - _event_log: { - key: 'notFound', - default: 'defaultValue', - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual('defaultValue'); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_event_log param object with zero default', async () => { - const input = { - _event_log: { - key: 'notFound', - default: 0, - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual(0); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_event_log param object with false default', async () => { - const input = { - _event_log: { - key: 'notFound', - default: false, - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_event_log param object with no default', async () => { - const input = { - _event_log: { - key: 'notFound', - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual(null); - expect(res.errors).toMatchInlineSnapshot(`Array []`); }); diff --git a/packages/plugins/operators/operators-js/src/operators/client/global.test.js b/packages/plugins/operators/operators-js/src/operators/client/global.test.js index 883d0318a..a155f41bc 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/global.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/global.test.js @@ -13,26 +13,20 @@ See the License for the specific language governing permissions and limitations under the License. */ +import _global from './global.js'; -jest.mock('@lowdefy/operators'); - -let lowdefyOperators; -let global; - -const input = { - arrayIndices: [0], - location: 'location', - lowdefyGlobal: { lowdefyGlobal: true }, - params: 'params', -}; - -beforeEach(async () => { - lowdefyOperators = await import('@lowdefy/operators'); - global = (await import('./global.js')).default; -}); +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); test('global calls getFromObject', async () => { - global(input); + const lowdefyOperators = await import('@lowdefy/operators'); + _global({ + arrayIndices: [0], + location: 'location', + params: 'params', + lowdefyGlobal: { lowdefyGlobal: true }, + }); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ { diff --git a/packages/plugins/operators/operators-js/src/operators/client/input.test.js b/packages/plugins/operators/operators-js/src/operators/client/input.test.js index f1233a255..66b7e856f 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/input.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/input.test.js @@ -15,18 +15,19 @@ */ import input from './input.js'; -jest.mock('@lowdefy/operators'); -const inputParams = { - arrayIndices: [0], - input: { input: true }, - location: 'location', - params: 'params', -}; +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); -test('input calls getFromObject', () => { - const lowdefyOperators = import('@lowdefy/operators'); - input(inputParams); +test('input calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); + input({ + arrayIndices: [0], + input: { input: true }, + location: 'location', + params: 'params', + }); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ { diff --git a/packages/plugins/operators/operators-js/src/operators/client/location.js b/packages/plugins/operators/operators-js/src/operators/client/location.js index 63678c668..7149bff97 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/location.js +++ b/packages/plugins/operators/operators-js/src/operators/client/location.js @@ -31,6 +31,7 @@ const validProperties = [ 'hash', ]; +// TODO: Fix with new router and link function _location({ arrayIndices, context, location, params }) { if (!window || !window.location) { throw new Error( diff --git a/packages/plugins/operators/operators-js/src/operators/client/location.test.js b/packages/plugins/operators/operators-js/src/operators/client/location.test.js index 802957cd3..2d903c275 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/location.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/location.test.js @@ -15,7 +15,10 @@ */ import location from './location.js'; -jest.mock('@lowdefy/operators'); + +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); beforeEach(() => { Object.defineProperty(window, 'location', { @@ -43,7 +46,7 @@ const input = { params: 'origin', }; -test('location calls getFromObject', () => { +test('location calls getFromObject', async () => { const lowdefyOperators = import('@lowdefy/operators'); location(input); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ diff --git a/packages/plugins/operators/operators-js/src/operators/client/request_details.test.js b/packages/plugins/operators/operators-js/src/operators/client/request_details.test.js index c722a05e2..f9814ce30 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/request_details.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/request_details.test.js @@ -14,182 +14,41 @@ limitations under the License. */ -/* eslint-disable max-classes-per-file */ -import { WebParser } from '@lowdefy/operators'; +import request_details from './request_details.js'; -const arrayIndices = [1]; +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); -const context = { - _internal: { - lowdefy: { - inputs: { id: true }, - lowdefyGlobal: { global: true }, - menus: [{ menus: true }], - urlQuery: { urlQuery: true }, - user: { user: true }, +test('request_details calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); + request_details({ + requests: { + request_id: { + response: 'returned from action', + loading: false, + error: [], + }, }, - }, - eventLog: [{ eventLog: true }], - id: 'id', - requests: [{ requests: true }], - state: { state: true }, -}; - -console.error = () => {}; - -test('_request_details in object', async () => { - const input = { _request_details: 'string' }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual({ loading: false, response: 'request String' }); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_request_details all requests', async () => { - const input = { _request_details: true }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual({ - not_loaded: { loading: true, response: 'fail' }, - string: { loading: false, response: 'request String' }, - number: { loading: false, response: 500 }, - arr: { loading: false, response: [{ a: 'request a1' }, { a: 'request a2' }] }, - returnsNull: { loading: false, response: null }, + arrayIndices: [0], + location: 'location', + params: 'params', }); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_request_details null', async () => { - const input = { _request_details: null }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _request_details params must be of type string, integer, boolean or object. Received: null at locationId.], - ] - `); -}); - -test('_request_details nested', async () => { - const input = { _request_details: 'string.response' }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual('request String'); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_request_details param object key', async () => { - const input = { - _request_details: { - key: 'string', - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual({ loading: false, response: 'request String' }); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_request_details param object all', async () => { - const input = { - _request_details: { - all: true, - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual({ - not_loaded: { loading: true, response: 'fail' }, - string: { loading: false, response: 'request String' }, - number: { loading: false, response: 500 }, - arr: { loading: false, response: [{ a: 'request a1' }, { a: 'request a2' }] }, - returnsNull: { loading: false, response: null }, - }); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_request_details param object all and key', async () => { - const input = { - _request_details: { - all: true, - key: 'string', - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual({ - not_loaded: { loading: true, response: 'fail' }, - string: { loading: false, response: 'request String' }, - number: { loading: false, response: 500 }, - arr: { loading: false, response: [{ a: 'request a1' }, { a: 'request a2' }] }, - returnsNull: { loading: false, response: null }, - }); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_request_details param object invalid', async () => { - const input = { - _request_details: { - other: true, - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _request_details.key must be of type string or integer. Received: {"other":true} at locationId.], - ] - `); -}); - -test('_request_details param array', async () => { - const input = { - _request_details: ['string'], - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _request_details params must be of type string, integer, boolean or object. Received: ["string"] at locationId.], - ] - `); -}); - -test('_request_details param object with string default', async () => { - const input = { - _request_details: { - key: 'notFound', - default: 'defaultValue', - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual('defaultValue'); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_request_details param object with no default', async () => { - const input = { - _request_details: { - key: 'notFound', - }, - }; - const parser = new WebParser({ context }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId', arrayIndices }); - expect(res.output).toEqual(null); - expect(res.errors).toMatchInlineSnapshot(`Array []`); + expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ + [ + { + arrayIndices: [0], + location: 'location', + object: { + request_id: { + response: 'returned from action', + loading: false, + error: [], + }, + }, + operator: '_request_details', + params: 'params', + }, + ], + ]); }); diff --git a/packages/plugins/operators/operators-js/src/operators/client/state.test.js b/packages/plugins/operators/operators-js/src/operators/client/state.test.js index a66cff3c0..cc03bb34d 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/state.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/state.test.js @@ -15,18 +15,19 @@ */ import state from './state.js'; -jest.mock('@lowdefy/operators'); -const input = { - arrayIndices: [0], - location: 'location', - params: 'params', - state: { state: true }, -}; +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); -test('state calls getFromObject', () => { - const lowdefyOperators = import('@lowdefy/operators'); - state(input); +test('state calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); + state({ + arrayIndices: [0], + location: 'location', + params: 'params', + state: { state: true }, + }); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ { diff --git a/packages/plugins/operators/operators-js/src/operators/client/url_query.test.js b/packages/plugins/operators/operators-js/src/operators/client/url_query.test.js index 323e58605..4c097d881 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/url_query.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/url_query.test.js @@ -15,19 +15,21 @@ */ import url_query from './url_query.js'; -jest.mock('@lowdefy/operators'); -const input = { - arrayIndices: [0], - location: 'location', - params: 'params', - secrets: { secrets: true }, - urlQuery: { urlQuery: true }, -}; +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); -test('url_query calls getFromObject', () => { - const lowdefyOperators = import('@lowdefy/operators'); - url_query(input); +const input = {}; + +test('url_query calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); + url_query({ + arrayIndices: [0], + location: 'location', + params: 'params', + urlQuery: { urlQuery: true }, + }); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ { diff --git a/packages/plugins/operators/operators-js/src/operators/server/payload.test.js b/packages/plugins/operators/operators-js/src/operators/server/payload.test.js index 4ef37e8fc..b92e7eb6e 100644 --- a/packages/plugins/operators/operators-js/src/operators/server/payload.test.js +++ b/packages/plugins/operators/operators-js/src/operators/server/payload.test.js @@ -15,17 +15,18 @@ */ import payload from './payload.js'; -jest.mock('@lowdefy/operators'); -const input = { - location: 'location', - params: 'params', - payload: { payload: true }, -}; +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); -test('payload calls getFromObject', () => { - const lowdefyOperators = import('@lowdefy/operators'); - payload(input); +test('payload calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); + payload({ + location: 'location', + params: 'params', + payload: { payload: true }, + }); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ { diff --git a/packages/plugins/operators/operators-js/src/operators/server/secret.test.js b/packages/plugins/operators/operators-js/src/operators/server/secret.test.js index 7d26a6f8a..af03a1616 100644 --- a/packages/plugins/operators/operators-js/src/operators/server/secret.test.js +++ b/packages/plugins/operators/operators-js/src/operators/server/secret.test.js @@ -15,12 +15,15 @@ */ import secret from './secret.js'; -jest.mock('@lowdefy/operators'); + +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); console.error = () => {}; -test('secret calls getFromObject', () => { - const lowdefyOperators = import('@lowdefy/operators'); +test('secret calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); secret({ arrayIndices: [0], location: 'location', @@ -41,8 +44,8 @@ test('secret calls getFromObject', () => { ]); }); -test('secret default value', () => { - const lowdefyOperators = import('@lowdefy/operators'); +test('secret default value', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); secret({ arrayIndices: [0], location: 'location', @@ -72,8 +75,8 @@ test('secret get all is not allowed', () => { ); }); -test('secret OpenID Connect and JSON web token secrets are filtered out', () => { - const lowdefyOperators = import('@lowdefy/operators'); +test('secret OpenID Connect and JSON web token secrets are filtered out', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); secret({ arrayIndices: [0], location: 'location', diff --git a/packages/plugins/operators/operators-js/src/operators/shared/args.test.js b/packages/plugins/operators/operators-js/src/operators/shared/args.test.js index 087a4c64e..5cf3ec92c 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/args.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/args.test.js @@ -13,7 +13,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -jest.mock('@lowdefy/operators'); +import args from './args.js'; + +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); const input = { args: [{ args: true }], @@ -23,9 +27,13 @@ const input = { }; test('args calls getFromObject', async () => { - const args = await import('./args.js'); const lowdefyOperators = await import('@lowdefy/operators'); - args.default(input); + args({ + args: [{ args: true }], + arrayIndices: [0], + location: 'location', + params: 'params', + }); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ { diff --git a/packages/plugins/operators/operators-js/src/operators/shared/get.test.js b/packages/plugins/operators/operators-js/src/operators/shared/get.test.js index 194bf7892..719bcd370 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/get.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/get.test.js @@ -14,19 +14,21 @@ limitations under the License. */ import get from './get.js'; -jest.mock('@lowdefy/operators'); -test('_get calls getFromObject', () => { - const lowdefyOperators = import('@lowdefy/operators'); - const input = { +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); + +test('_get calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); + get({ arrayIndices: [0], location: 'location', params: { from: { a: 1 }, key: 'a', }, - }; - get.default(input); + }); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ { @@ -52,7 +54,6 @@ test('_get returns null if from is null', () => { key: 'a', }, }; - get(input); expect(get(input)).toBe(null); }); @@ -66,7 +67,6 @@ test('_get returns default value if from is null', () => { default: 'default', }, }; - get(input); expect(get(input)).toBe('default'); }); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/user.test.js b/packages/plugins/operators/operators-js/src/operators/shared/user.test.js index 14e5c997d..64d754916 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/user.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/user.test.js @@ -15,7 +15,9 @@ */ import user from './user.js'; -jest.mock('@lowdefy/operators'); +jest.mock('@lowdefy/operators', () => ({ + getFromObject: jest.fn(), +})); const input = { arrayIndices: [0], @@ -24,9 +26,14 @@ const input = { user: { name: 'first name' }, }; -test('user calls getFromObject', () => { - const lowdefyOperators = import('@lowdefy/operators'); - user(input); +test('user calls getFromObject', async () => { + const lowdefyOperators = await import('@lowdefy/operators'); + user({ + arrayIndices: [0], + location: 'location', + params: 'params', + user: { name: 'first name' }, + }); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ { From b9633e4f105c07efbf192f09038438a0f61b0390 Mon Sep 17 00:00:00 2001 From: Gervwyk Date: Mon, 7 Feb 2022 22:41:20 +0200 Subject: [PATCH 06/21] feat(engine): Update link to include noLink and disableLink, fix logic. --- packages/engine/src/createLink.js | 54 ++-- packages/engine/test/createLink.test.js | 325 +++++++++++++++++++++--- 2 files changed, 325 insertions(+), 54 deletions(-) diff --git a/packages/engine/src/createLink.js b/packages/engine/src/createLink.js index 4e344e88a..151d08fcd 100644 --- a/packages/engine/src/createLink.js +++ b/packages/engine/src/createLink.js @@ -16,32 +16,42 @@ import { type, urlQuery as urlQueryFn } from '@lowdefy/helpers'; -function createLink({ backLink, lowdefy, newOriginLink, sameOriginLink }) { - function link({ back, home, input, newTab, pageId, url, urlQuery }) { - let pathname = pageId; - if (back) { - return backLink(); +function createLink({ backLink, disabledLink, lowdefy, newOriginLink, noLink, sameOriginLink }) { + function link(props) { + if (props.disabled === true) { + return disabledLink(props); } - const lowdefyUrlQuery = type.isNone(urlQuery) ? '' : `?${urlQueryFn.stringify(urlQuery)}`; - if (home) { - if (lowdefy.home.configured) { - pathname = ''; - pageId = lowdefy.home.pageId; - } else { - pathname = lowdefy.home.pageId; - pageId = lowdefy.home.pageId; - } + if ([!props.pageId, !props.back, !props.home, !props.url].filter((v) => !v).length > 1) { + throw Error( + `Invalid Link: To avoid ambiguity, only one of 'back', 'home', 'pageId' or 'url' can be defined.` + ); } - if (!type.isNone(pathname)) { - if (!type.isNone(input)) { - lowdefy.inputs[pageId] = input; - } - return sameOriginLink(`/${pathname}${lowdefyUrlQuery}`, newTab); + if (props.back === true) { + // Cannot set input or urlQuery on back + return backLink(props); } - if (!type.isNone(url)) { - return newOriginLink(`${url}${lowdefyUrlQuery}`, newTab); + const lowdefyUrlQuery = type.isNone(props.urlQuery) + ? '' + : `?${urlQueryFn.stringify(props.urlQuery)}`; + if (props.home === true) { + lowdefy.inputs[`page:${lowdefy.home.pageId}`] = props.input; + return sameOriginLink({ + href: `/${lowdefy.home.configured ? '' : lowdefy.home.pageId}${lowdefyUrlQuery}`, + ...props, + }); } - throw new Error(`Invalid Link.`); + if (type.isString(props.pageId)) { + lowdefy.inputs[`page:${props.pageId}`] = props.input; + return sameOriginLink({ href: `/${props.pageId}${lowdefyUrlQuery}`, ...props }); + } + if (type.isString(props.url)) { + const protocol = props.url.includes(':') ? '' : 'https://'; + return newOriginLink({ + href: `${protocol}${props.url}${lowdefyUrlQuery}`, + ...props, + }); + } + return noLink(props); } return link; } diff --git a/packages/engine/test/createLink.test.js b/packages/engine/test/createLink.test.js index a9fddd287..54be8b03b 100644 --- a/packages/engine/test/createLink.test.js +++ b/packages/engine/test/createLink.test.js @@ -1,12 +1,16 @@ import createLink from '../src/createLink.js'; const mockBackLink = jest.fn(); +const mockDisabledLink = jest.fn(); const mockNewOriginLink = jest.fn(); +const mockNoLink = jest.fn(); const mockSameOriginLink = jest.fn(); beforeEach(() => { mockBackLink.mockReset(); + mockDisabledLink.mockReset(); mockNewOriginLink.mockReset(); + mockNoLink.mockReset(); mockSameOriginLink.mockReset(); }); @@ -14,17 +18,34 @@ test('createLink, link with pageId', () => { const lowdefy = { inputs: {} }; const link = createLink({ backLink: mockBackLink, + disabledLink: mockDisabledLink, lowdefy, newOriginLink: mockNewOriginLink, + noLink: mockNoLink, sameOriginLink: mockSameOriginLink, }); link({ pageId: 'page_1' }); link({ pageId: 'page_1', urlQuery: { p: 3 } }); expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([]); expect(mockNewOriginLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([]); expect(mockSameOriginLink.mock.calls).toEqual([ - ['/page_1', undefined], - ['/page_1?p=3', undefined], + [ + { + href: '/page_1', + pageId: 'page_1', + }, + ], + [ + { + href: '/page_1?p=3', + pageId: 'page_1', + urlQuery: { + p: 3, + }, + }, + ], ]); }); @@ -32,17 +53,36 @@ test('createLink, link with pageId new tab', () => { const lowdefy = { inputs: {} }; const link = createLink({ backLink: mockBackLink, + disabledLink: mockDisabledLink, lowdefy, newOriginLink: mockNewOriginLink, + noLink: mockNoLink, sameOriginLink: mockSameOriginLink, }); link({ pageId: 'page_1', newTab: true }); link({ pageId: 'page_1', newTab: true, urlQuery: { p: 3 } }); expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([]); expect(mockNewOriginLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([]); expect(mockSameOriginLink.mock.calls).toEqual([ - ['/page_1', true], - ['/page_1?p=3', true], + [ + { + href: '/page_1', + pageId: 'page_1', + newTab: true, + }, + ], + [ + { + href: '/page_1?p=3', + pageId: 'page_1', + newTab: true, + urlQuery: { + p: 3, + }, + }, + ], ]); }); @@ -50,145 +90,366 @@ test('createLink, link with pageId with inputs', () => { const lowdefy = { inputs: {} }; const link = createLink({ backLink: mockBackLink, + disabledLink: mockDisabledLink, lowdefy, newOriginLink: mockNewOriginLink, + noLink: mockNoLink, sameOriginLink: mockSameOriginLink, }); link({ pageId: 'page_1', input: { a: 1 } }); link({ pageId: 'page_2', input: { a: 2 }, urlQuery: { p: 3 } }); expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([]); expect(mockNewOriginLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([]); expect(mockSameOriginLink.mock.calls).toEqual([ - ['/page_1', undefined], - ['/page_2?p=3', undefined], + [ + { + href: '/page_1', + input: { + a: 1, + }, + pageId: 'page_1', + }, + ], + [ + { + href: '/page_2?p=3', + input: { + a: 2, + }, + pageId: 'page_2', + urlQuery: { + p: 3, + }, + }, + ], ]); expect(lowdefy.inputs).toEqual({ - page_1: { a: 1 }, - page_2: { a: 2 }, + 'page:page_1': { a: 1 }, + 'page:page_2': { a: 2 }, }); }); -test('createLink, link with url', () => { +test('createLink, link with url and protocol', () => { const lowdefy = { inputs: {} }; const link = createLink({ backLink: mockBackLink, + disabledLink: mockDisabledLink, lowdefy, newOriginLink: mockNewOriginLink, + noLink: mockNoLink, sameOriginLink: mockSameOriginLink, }); link({ url: 'http://localhost:8080/test' }); link({ url: 'http://localhost:8080/test', urlQuery: { p: 3 } }); expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([]); expect(mockNewOriginLink.mock.calls).toEqual([ - ['http://localhost:8080/test', undefined], - ['http://localhost:8080/test?p=3', undefined], + [ + { + href: 'http://localhost:8080/test', + url: 'http://localhost:8080/test', + }, + ], + [ + { + href: 'http://localhost:8080/test?p=3', + url: 'http://localhost:8080/test', + urlQuery: { + p: 3, + }, + }, + ], ]); expect(mockSameOriginLink.mock.calls).toEqual([]); }); -test('createLink, link with url new tab', () => { +test('createLink, link with url new tab and protocol', () => { const lowdefy = { inputs: {} }; const link = createLink({ backLink: mockBackLink, + disabledLink: mockDisabledLink, lowdefy, newOriginLink: mockNewOriginLink, + noLink: mockNoLink, sameOriginLink: mockSameOriginLink, }); link({ url: 'http://localhost:8080/test', newTab: true }); link({ url: 'http://localhost:8080/test', newTab: true, urlQuery: { p: 3 } }); expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([]); expect(mockNewOriginLink.mock.calls).toEqual([ - ['http://localhost:8080/test', true], - ['http://localhost:8080/test?p=3', true], + [ + { + href: 'http://localhost:8080/test', + url: 'http://localhost:8080/test', + newTab: true, + }, + ], + [ + { + href: 'http://localhost:8080/test?p=3', + url: 'http://localhost:8080/test', + urlQuery: { + p: 3, + }, + newTab: true, + }, + ], ]); expect(mockSameOriginLink.mock.calls).toEqual([]); }); -test('createLink, link with home', () => { - const lowdefy = { inputs: {}, homePageId: 'home' }; +test('createLink, link with url and no protocol', () => { + const lowdefy = { inputs: {} }; const link = createLink({ backLink: mockBackLink, + disabledLink: mockDisabledLink, lowdefy, newOriginLink: mockNewOriginLink, + noLink: mockNoLink, + sameOriginLink: mockSameOriginLink, + }); + link({ url: 'external.com/test', newTab: true }); + link({ url: 'external.com/test', newTab: true, urlQuery: { p: 3 } }); + expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([]); + expect(mockNewOriginLink.mock.calls).toEqual([ + [ + { + href: 'https://external.com/test', + url: 'external.com/test', + newTab: true, + }, + ], + [ + { + href: 'https://external.com/test?p=3', + url: 'external.com/test', + urlQuery: { + p: 3, + }, + newTab: true, + }, + ], + ]); + expect(mockSameOriginLink.mock.calls).toEqual([]); +}); + +test('createLink, link with home, not configured', () => { + const lowdefy = { inputs: {}, home: { pageId: 'home', configured: false } }; + const link = createLink({ + backLink: mockBackLink, + disabledLink: mockDisabledLink, + lowdefy, + newOriginLink: mockNewOriginLink, + noLink: mockNoLink, sameOriginLink: mockSameOriginLink, }); link({ home: true }); link({ home: true, urlQuery: { p: 3 } }); expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([]); expect(mockNewOriginLink.mock.calls).toEqual([]); expect(mockSameOriginLink.mock.calls).toEqual([ - ['/home', undefined], - ['/home?p=3', undefined], + [ + { + home: true, + href: '/home', + }, + ], + [ + { + home: true, + href: '/home?p=3', + urlQuery: { + p: 3, + }, + }, + ], ]); }); -test('createLink, link with home new tab', () => { - const lowdefy = { inputs: {}, homePageId: 'home' }; +test('createLink, link with home, configured', () => { + const lowdefy = { inputs: {}, home: { pageId: 'home', configured: true } }; const link = createLink({ backLink: mockBackLink, + disabledLink: mockDisabledLink, lowdefy, newOriginLink: mockNewOriginLink, + noLink: mockNoLink, + sameOriginLink: mockSameOriginLink, + }); + link({ home: true }); + link({ home: true, urlQuery: { p: 3 } }); + expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([]); + expect(mockNewOriginLink.mock.calls).toEqual([]); + expect(mockSameOriginLink.mock.calls).toEqual([ + [ + { + home: true, + href: '/', + }, + ], + [ + { + home: true, + href: '/?p=3', + urlQuery: { + p: 3, + }, + }, + ], + ]); +}); + +test('createLink, link with home new tab, not configured', () => { + const lowdefy = { inputs: {}, home: { pageId: 'home' } }; + const link = createLink({ + backLink: mockBackLink, + disabledLink: mockDisabledLink, + lowdefy, + newOriginLink: mockNewOriginLink, + noLink: mockNoLink, sameOriginLink: mockSameOriginLink, }); link({ home: true, newTab: true }); link({ home: true, newTab: true, urlQuery: { p: 3 } }); expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([]); expect(mockNewOriginLink.mock.calls).toEqual([]); expect(mockSameOriginLink.mock.calls).toEqual([ - ['/home', true], - ['/home?p=3', true], + [{ home: true, href: '/home', newTab: true }], + [ + { + home: true, + href: '/home?p=3', + newTab: true, + urlQuery: { + p: 3, + }, + }, + ], ]); }); -test('createLink, link with home with inputs', () => { - const lowdefy = { inputs: {}, homePageId: 'home' }; +test('createLink, link with home with inputs, not configured', () => { + const lowdefy = { inputs: {}, home: { pageId: 'home' } }; const link = createLink({ backLink: mockBackLink, + disabledLink: mockDisabledLink, lowdefy, newOriginLink: mockNewOriginLink, + noLink: mockNoLink, sameOriginLink: mockSameOriginLink, }); link({ home: true, input: { a: 1 } }); expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([]); expect(mockNewOriginLink.mock.calls).toEqual([]); - expect(mockSameOriginLink.mock.calls).toEqual([['/home', undefined]]); + expect(mockSameOriginLink.mock.calls).toEqual([ + [ + { + home: true, + href: '/home', + input: { + a: 1, + }, + }, + ], + ]); expect(lowdefy.inputs).toEqual({ - home: { a: 1 }, + 'page:home': { a: 1 }, }); }); -test('createLink, link to throw if no params', () => { - const lowdefy = { inputs: {}, homePageId: 'home' }; +test('createLink, no params calls noLink', () => { + const lowdefy = { inputs: {}, home: { pageId: 'home' } }; const link = createLink({ backLink: mockBackLink, + disabledLink: mockDisabledLink, lowdefy, newOriginLink: mockNewOriginLink, + noLink: mockNoLink, sameOriginLink: mockSameOriginLink, }); - expect(() => link({})).toThrowErrorMatchingInlineSnapshot(`"Invalid Link."`); + link({}); + expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([[{}]]); + expect(mockNewOriginLink.mock.calls).toEqual([]); + expect(mockSameOriginLink.mock.calls).toEqual([]); +}); + +test('createLink, disabled calls disabledLink', () => { + const lowdefy = { inputs: {}, home: { pageId: 'home' } }; + const link = createLink({ + backLink: mockBackLink, + disabledLink: mockDisabledLink, + lowdefy, + newOriginLink: mockNewOriginLink, + noLink: mockNoLink, + sameOriginLink: mockSameOriginLink, + }); + link({ disabled: true, home: true }); + expect(mockBackLink.mock.calls).toEqual([]); + expect(mockDisabledLink.mock.calls).toEqual([ + [ + { + disabled: true, + home: true, + }, + ], + ]); + expect(mockNoLink.mock.calls).toEqual([]); + expect(mockNewOriginLink.mock.calls).toEqual([]); + expect(mockSameOriginLink.mock.calls).toEqual([]); }); test('createLink, link with back', () => { const lowdefy = { inputs: {} }; const link = createLink({ backLink: mockBackLink, + disabledLink: mockDisabledLink, lowdefy, newOriginLink: mockNewOriginLink, + noLink: mockNoLink, sameOriginLink: mockSameOriginLink, }); link({ back: true }); - expect(mockBackLink.mock.calls).toEqual([[]]); + expect(mockBackLink.mock.calls).toEqual([ + [ + { + back: true, + }, + ], + ]); + expect(mockDisabledLink.mock.calls).toEqual([]); + expect(mockNoLink.mock.calls).toEqual([]); expect(mockNewOriginLink.mock.calls).toEqual([]); expect(mockSameOriginLink.mock.calls).toEqual([]); }); -test('createLink, link with back equal to false is invalid', () => { +test('createLink, link with more than one parameter is invalid.', () => { const lowdefy = { inputs: {} }; const link = createLink({ backLink: mockBackLink, + disabledLink: mockDisabledLink, lowdefy, newOriginLink: mockNewOriginLink, + noLink: mockNoLink, sameOriginLink: mockSameOriginLink, }); - expect(() => link({ back: false })).toThrowErrorMatchingInlineSnapshot(`"Invalid Link."`); + expect(() => link({ back: true, home: true })).toThrowErrorMatchingInlineSnapshot( + `"Invalid Link: To avoid ambiguity, only one of 'back', 'home', 'pageId' or 'url' can be defined."` + ); }); From 6104ae0254909fa969bd9f641e540700d8d8b268 Mon Sep 17 00:00:00 2001 From: Gervwyk Date: Mon, 7 Feb 2022 22:44:44 +0200 Subject: [PATCH 07/21] feat(server): Add Next Link component implementation. --- packages/server/src/components/Page.js | 9 +-- .../{components.js => createComponents.js} | 12 ++-- .../src/components/createLinkComponent.js | 62 +++++++++++++++++++ packages/server/src/utils/setupLink.js | 31 +++++----- 4 files changed, 91 insertions(+), 23 deletions(-) rename packages/server/src/components/{components.js => createComponents.js} (75%) create mode 100644 packages/server/src/components/createLinkComponent.js diff --git a/packages/server/src/components/Page.js b/packages/server/src/components/Page.js index e7cef0590..f8347d76a 100644 --- a/packages/server/src/components/Page.js +++ b/packages/server/src/components/Page.js @@ -16,25 +16,26 @@ import React from 'react'; +import { urlQuery } from '@lowdefy/helpers'; import { useRouter } from 'next/router'; import Context from './Context.js'; import Head from './Head.js'; import Block from './block/Block.js'; import setupLink from '../utils/setupLink.js'; +import createComponents from './createComponents.js'; const LoadingBlock = () =>
Loading...
; const Page = ({ lowdefy, pageConfig, rootConfig }) => { const router = useRouter(); - lowdefy._internal.basePath = router.basePath; - lowdefy._internal.pathname = router.pathname; - lowdefy._internal.query = router.query; lowdefy._internal.router = router; - lowdefy._internal.link = setupLink({ lowdefy }); + lowdefy._internal.link = setupLink(lowdefy); + lowdefy._internal.components = createComponents(lowdefy); lowdefy.home = rootConfig.home; lowdefy.lowdefyGlobal = rootConfig.lowdefyGlobal; lowdefy.menus = rootConfig.menus; + lowdefy.urlQuery = urlQuery.parse(window.location.search.slice(1)); return ( {(context, loading) => { diff --git a/packages/server/src/components/components.js b/packages/server/src/components/createComponents.js similarity index 75% rename from packages/server/src/components/components.js rename to packages/server/src/components/createComponents.js index 0054706a2..587496cfb 100644 --- a/packages/server/src/components/components.js +++ b/packages/server/src/components/createComponents.js @@ -14,12 +14,16 @@ limitations under the License. */ -import Link from 'next/link'; import { createIcon } from '@lowdefy/block-utils'; +import createLinkComponent from './createLinkComponent.js'; import icons from '../../build/plugins/icons.js'; -export default { - Link, - Icon: createIcon(icons), +const createComponents = (lowdefy) => { + return { + Link: createLinkComponent(lowdefy), + Icon: createIcon(icons), + }; }; + +export default createComponents; diff --git a/packages/server/src/components/createLinkComponent.js b/packages/server/src/components/createLinkComponent.js new file mode 100644 index 000000000..f82679316 --- /dev/null +++ b/packages/server/src/components/createLinkComponent.js @@ -0,0 +1,62 @@ +import React from 'react'; +import NextLink from 'next/link'; +import { createLink } from '@lowdefy/engine'; +import { type } from '@lowdefy/helpers'; + +const createLinkComponent = (lowdefy) => { + const backLink = ({ children, className, id }) => ( + lowdefy._internal.router.back()} className={className}> + {type.isFunction(children) ? children(id) : children} + + ); + const newOriginLink = ({ children, className, href, id, newTab, pageId, url }) => { + return ( + + {type.isFunction(children) ? children(pageId || url || id) : children} + + ); + }; + const sameOriginLink = ({ children, className, href, id, newTab, pageId, url }) => { + if (newTab) { + return ( + + {type.isFunction(children) ? children(pageId || url || id) : children} + + ); + } + return ( + + + {type.isFunction(children) ? children(pageId || url || id) : children} + + + ); + }; + const noLink = ({ className, children, id }) => ( + + {type.isFunction(children) ? children(id) : children} + + ); + return createLink({ + backLink, + lowdefy, + newOriginLink, + sameOriginLink, + noLink, + disabledLink: noLink, + }); +}; + +export default createLinkComponent; diff --git a/packages/server/src/utils/setupLink.js b/packages/server/src/utils/setupLink.js index 34077d516..a41042aae 100644 --- a/packages/server/src/utils/setupLink.js +++ b/packages/server/src/utils/setupLink.js @@ -16,29 +16,30 @@ import { createLink } from '@lowdefy/engine'; -function setupLink({ lowdefy }) { +function setupLink(lowdefy) { const { router, window } = lowdefy._internal; - const sameOriginLink = (path, newTab) => { + const backLink = () => router.back(); + const disabledLink = () => {}; + const newOriginLink = ({ href, newTab }) => { if (newTab) { - return window.open(`${window.location.origin}${lowdefy.basePath}${path}`, '_blank').focus(); + return window.open(href, '_blank').focus(); + } else { + return window.location.assign(href); + } + }; + const sameOriginLink = ({ href, newTab }) => { + if (newTab) { + return window.open(`${window.location.origin}${lowdefy.basePath}${href}`, '_blank').focus(); } else { - // Next handles the basePath here. return router.push({ - pathname: path, - // TODO: Do we handle urlQuery as a param here? - // query: {}, + pathname: href, // href includes the urlQuery as defined by engine }); } }; - const newOriginLink = (path, newTab) => { - if (newTab) { - return window.open(path, '_blank').focus(); - } else { - return (window.location.href = path); - } + const noLink = () => { + throw new Error(`Invalid Link.`); }; - const backLink = () => window.history.back(); - return createLink({ backLink, lowdefy, newOriginLink, sameOriginLink }); + return createLink({ backLink, disabledLink, lowdefy, newOriginLink, noLink, sameOriginLink }); } export default setupLink; From 4b3d061a0de79b86b0fa8be9ff8948a9dc0caeb7 Mon Sep 17 00:00:00 2001 From: Gervwyk Date: Mon, 7 Feb 2022 22:45:39 +0200 Subject: [PATCH 08/21] fix(server): Maintain the lowdefy objects during page transitions. --- .../server/src/components/LowdefyContext.js | 20 +++++++++---------- packages/server/src/pages/_app.js | 6 ++++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/server/src/components/LowdefyContext.js b/packages/server/src/components/LowdefyContext.js index 257d37240..9b1402503 100644 --- a/packages/server/src/components/LowdefyContext.js +++ b/packages/server/src/components/LowdefyContext.js @@ -19,14 +19,12 @@ import React from 'react'; import callRequest from '../utils/callRequest.js'; import blockComponents from '../../build/plugins/blocks.js'; import operators from '../../build/plugins/operatorsClient.js'; -import components from './components.js'; -const LowdefyContext = ({ children }) => { - const lowdefy = { - _internal: { +const LowdefyContext = ({ children, lowdefy }) => { + if (!lowdefy._internal) { + lowdefy._internal = { blockComponents, callRequest, - components, document, operators, updaters: {}, @@ -36,14 +34,14 @@ const LowdefyContext = ({ children }) => { return () => undefined; }, link: () => undefined, - }, - contexts: {}, - inputs: {}, - lowdefyGlobal: {}, - }; + }; + lowdefy.contexts = {}; + lowdefy.inputs = {}; + lowdefy.lowdefyGlobal = {}; + } lowdefy._internal.updateBlock = (blockId) => lowdefy._internal.updaters[blockId] && lowdefy._internal.updaters[blockId](); - return <>{children(lowdefy)}; + return <>{children}; }; export default LowdefyContext; diff --git a/packages/server/src/pages/_app.js b/packages/server/src/pages/_app.js index eb3785ad5..b3a4088f3 100644 --- a/packages/server/src/pages/_app.js +++ b/packages/server/src/pages/_app.js @@ -22,12 +22,14 @@ import LowdefyContext from '../components/LowdefyContext.js'; import '../../build/plugins/styles.less'; +const lowdefy = {}; + function App({ Component, pageProps }) { return ( - - {(lowdefy) => } + + From 37b80b172bc078ab4b83f5932fc8d0908c5baf6f Mon Sep 17 00:00:00 2001 From: Gervwyk Date: Mon, 7 Feb 2022 23:23:32 +0200 Subject: [PATCH 09/21] feat(docs): Add docs for the Link component. --- packages/docs/concepts/custom-blocks.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/docs/concepts/custom-blocks.yaml b/packages/docs/concepts/custom-blocks.yaml index 9932565bb..8f78ee1f0 100644 --- a/packages/docs/concepts/custom-blocks.yaml +++ b/packages/docs/concepts/custom-blocks.yaml @@ -123,6 +123,16 @@ _ref: - `basePath: string`: The base path setting for the application. This variable is used to prefix route paths for example `${basePath}/public/logo-square-light-theme.png`. The default base path is ''. - `blockId: string`: The block's id within the Lowdefy app, this is useful for setting a unique `id` on DOM elements. + - `components: object`: Helper React components that are exposed to blocks to use internally. + - `Icon`: component`: Lowdefy standard Icon React component to render build in icons. + - `Link`: component`: Lowdefy standard Link React component used as links to pages or external urls. The following props apply: + - `back: boolean`: When the link is clicked, trigger the browser back. + - `home: boolean`: When the link is clicked, route to the home page. + - `pageId: string`: When the link is clicked, route to the provided Lowdefy page. + - `url: string`: When the link is clicked, route to an external url. + - `newTab: boolean`: When the link is clicked, open the page in a new browser tab. + - `input: object`: When the link is clicked, pass data as the input object to the next Lowdefy page. See [Input]( TODO: Link to input page? ). + - `urlQuery: object`: When the link is clicked, pass data as a url query to the next Lowdefy page. See [url query]( TODO: Link to url query page? ). - `content: object`: Passed to `container` and `context` block categories. The `content` object with methods to render sub blocks into content areas. The method name is the same as the area key, for example, 'content.content()` renders a blocks default `content` area. - `events: object`: All events defined on the block in the Lowdefy app config. - `[event_key]: object`: Event keys gives a handle name to the event details. From 2bcf600bd1ae477325cf205069952006e3032b63 Mon Sep 17 00:00:00 2001 From: Gervwyk Date: Mon, 7 Feb 2022 23:29:09 +0200 Subject: [PATCH 10/21] feat(blocks): Implement Link in blocks. --- .../src/blocks/Breadcrumb/Breadcrumb.js | 54 ++++----- .../blocks-antd/src/blocks/Menu/Menu.js | 104 +++++++----------- .../blocks/PageHeaderMenu/PageHeaderMenu.js | 4 +- .../src/blocks/PageSiderMenu/PageSiderMenu.js | 4 +- .../blocks-basic/src/blocks/Anchor/Anchor.js | 60 +++------- 5 files changed, 76 insertions(+), 150 deletions(-) diff --git a/packages/plugins/blocks/blocks-antd/src/blocks/Breadcrumb/Breadcrumb.js b/packages/plugins/blocks/blocks-antd/src/blocks/Breadcrumb/Breadcrumb.js index 2185804b3..33cf4a200 100644 --- a/packages/plugins/blocks/blocks-antd/src/blocks/Breadcrumb/Breadcrumb.js +++ b/packages/plugins/blocks/blocks-antd/src/blocks/Breadcrumb/Breadcrumb.js @@ -19,26 +19,7 @@ import { type, get } from '@lowdefy/helpers'; import { Breadcrumb } from 'antd'; import { blockDefaultProps } from '@lowdefy/block-utils'; -const ItemLink = ({ basePath, children, className, link, Link }) => { - if (type.isString(link.pageId)) { - return ( - - {children} - - ); - } - if (type.isString(link.url)) { - return ( - - {children} - - ); - } - return {children}; -}; - const BreadcrumbBlock = ({ - basePath, blockId, events, components: { Icon, Link }, @@ -61,30 +42,33 @@ const BreadcrumbBlock = ({ (() => methods.triggerEvent({ name: onClickActionName, event: { link, index } })) } > - - {link.icon && ( - + {(defaultTitle) => ( + <> + {link.icon && ( + + )} + {type.isString(link) ? link : link.label || defaultTitle} + )} - {type.isString(link) ? link : link.label || link.pageId || link.url || `Link ${index}`} - + ))} diff --git a/packages/plugins/blocks/blocks-antd/src/blocks/Menu/Menu.js b/packages/plugins/blocks/blocks-antd/src/blocks/Menu/Menu.js index a3f4210be..ea98e9c53 100644 --- a/packages/plugins/blocks/blocks-antd/src/blocks/Menu/Menu.js +++ b/packages/plugins/blocks/blocks-antd/src/blocks/Menu/Menu.js @@ -28,26 +28,8 @@ const getDefaultMenu = (menus, menuId = 'default', links) => { return menu.links || []; }; -const getTitle = (id, properties, defaultTitle) => - (properties && properties.title) || defaultTitle || id; - -const MenuTitle = ({ basePath, id, Link, linkStyle, makeCssClass, pageId, properties, url }) => { - if (type.isString(pageId)) { - return ( - - {getTitle(id, properties, pageId)} - - ); - } - if (url) { - return ( - - {getTitle(id, properties, url)} - - ); - } - return {getTitle(id, properties)}; -}; +const getTitle = ({ id, properties, pageId, url }) => + (properties && properties.title) || pageId || url || id; const getNestedColors = (menuColor, background) => { const fontColor = color(menuColor, 6); @@ -69,7 +51,6 @@ const getNestedColors = (menuColor, background) => { }; const MenuComp = ({ - basePath, blockId, components: { Icon, Link }, events, @@ -218,14 +199,13 @@ const MenuComp = ({ ])} key={`${link.pageId || link.id}_${i}`} title={ - + + {getTitle(link)} + } icon={ link.properties && @@ -253,14 +233,13 @@ const MenuComp = ({ + + {getTitle(subLink)} + } > {subLink.links.map((subLinkGroup, k) => { @@ -288,16 +267,13 @@ const MenuComp = ({ ) } > - + + {getTitle(subLinkGroup)} + ); })} @@ -320,16 +296,13 @@ const MenuComp = ({ ) } > - + + {getTitle(subLink)} + ); } @@ -353,16 +326,13 @@ const MenuComp = ({ ) } > - + + {getTitle(link)} + ); } diff --git a/packages/plugins/blocks/blocks-antd/src/blocks/PageHeaderMenu/PageHeaderMenu.js b/packages/plugins/blocks/blocks-antd/src/blocks/PageHeaderMenu/PageHeaderMenu.js index b66f6a8b5..3027d2404 100644 --- a/packages/plugins/blocks/blocks-antd/src/blocks/PageHeaderMenu/PageHeaderMenu.js +++ b/packages/plugins/blocks/blocks-antd/src/blocks/PageHeaderMenu/PageHeaderMenu.js @@ -32,7 +32,6 @@ const PageHeaderMenu = ({ components: { Icon, Link }, content, events, - homePageId, menus, methods, pageId, @@ -116,10 +115,9 @@ const PageHeaderMenu = ({ ])} content={{ // TODO: use next/image - // TODO: Link to home=true content: () => ( <> - + ( <>
- + (strong ? {children} : <>{children}); -const Tag = ({ blockId, children, className, disabled, href, Link, newTab, onClick, rel }) => - disabled ? ( - - {children} - - ) : ( - - {children} - - ); - const AnchorBlock = ({ blockId, events, @@ -45,38 +26,33 @@ const AnchorBlock = ({ methods, properties, }) => { - const title = type.isNone(properties.title) - ? type.isNone(properties.href) - ? properties.href - : blockId - : properties.title; const showLoading = get(events, 'onClick.loading') || loading; const disabled = properties.disabled || showLoading; return ( - methods.triggerEvent({ name: 'onClick' })} + {...properties} > - - {properties.icon && ( - - )} - {` ${title}`} - - + {(defaultTitle) => ( + <> + {properties.icon && + ( + + ) + ` `} + {properties.title || defaultTitle} + + )} + ); }; From 1cce024339bc89e4192d86f09d1a9ec233663f02 Mon Sep 17 00:00:00 2001 From: Gervwyk Date: Tue, 8 Feb 2022 15:17:52 +0200 Subject: [PATCH 11/21] feat(build): Add buildPath to config. --- packages/build/src/build/validateConfig.js | 5 ++++ .../build/src/build/validateConfig.test.js | 28 +++++++++++++++++++ packages/build/src/lowdefySchema.js | 7 +++++ packages/docs/concepts/lowdefy-schema.yaml | 1 + packages/docs/deployment/docker.yaml | 2 +- packages/docs/deployment/node-server.yaml | 2 +- packages/server-dev/next.config.js | 1 + packages/server/next.config.js | 2 ++ .../server/src/components/LowdefyContext.js | 1 + packages/server/src/components/Page.js | 3 ++ .../src/components/block/CategorySwitch.js | 7 ++--- .../server/src/components/block/Container.js | 3 +- packages/server/src/components/block/List.js | 3 +- packages/server/src/pages/index.js | 1 + packages/utils/block-utils/src/createIcon.js | 1 - .../utils/node-utils/src/getConfigFromEnv.js | 2 +- .../node-utils/src/getConfigFromEnv.test.js | 2 +- 17 files changed, 57 insertions(+), 14 deletions(-) diff --git a/packages/build/src/build/validateConfig.js b/packages/build/src/build/validateConfig.js index 048692f93..676047c8d 100644 --- a/packages/build/src/build/validateConfig.js +++ b/packages/build/src/build/validateConfig.js @@ -39,6 +39,11 @@ async function validateConfig({ components }) { if (type.isNone(components.config.theme)) { components.config.theme = {}; } + if (type.isString(components.config.basePath)) { + if (components.config.basePath[0] !== '/') { + throw Error('Base path must start with "/".'); + } + } validate({ schema: lowdefySchema.definitions.authConfig, data: components.config.auth, diff --git a/packages/build/src/build/validateConfig.test.js b/packages/build/src/build/validateConfig.test.js index e2b8be085..42483c788 100644 --- a/packages/build/src/build/validateConfig.test.js +++ b/packages/build/src/build/validateConfig.test.js @@ -155,3 +155,31 @@ test('validateConfig config error when protected or public are false.', async () 'Public pages can not be set to false.' ); }); + +test('validateConfig config error when basePath does not start with "/".', async () => { + let components = { + config: { + basePath: '/base', + }, + }; + const result = await validateConfig({ components, context }); + expect(result).toEqual({ + config: { + auth: { + pages: { + roles: {}, + }, + }, + basePath: '/base', + theme: {}, + }, + }); + components = { + config: { + basePath: 'base', + }, + }; + await expect(validateConfig({ components, context })).rejects.toThrow( + 'Base path must start with "/".' + ); +}); diff --git a/packages/build/src/lowdefySchema.js b/packages/build/src/lowdefySchema.js index 63aa33541..efc069897 100644 --- a/packages/build/src/lowdefySchema.js +++ b/packages/build/src/lowdefySchema.js @@ -598,6 +598,13 @@ export default { auth: { $ref: '#/definitions/authConfig', }, + basePath: { + type: 'string', + description: 'App base path to apply to all routes. Base path must start with "/".', + errorMessage: { + type: 'App "config.basePath" should be a string.', + }, + }, homePageId: { type: 'string', description: diff --git a/packages/docs/concepts/lowdefy-schema.yaml b/packages/docs/concepts/lowdefy-schema.yaml index cff79d3b9..74818b5ed 100644 --- a/packages/docs/concepts/lowdefy-schema.yaml +++ b/packages/docs/concepts/lowdefy-schema.yaml @@ -42,6 +42,7 @@ _ref: The config object has the following properties: + - `basePath: string`: Set the base path to serve the Lowdefy application from. This will route all pages under `https://example.com//` instead of the default `https://example.com/`. The basePath value must start with "/". - `homePageId: string`: The pageId of the page that should be loaded when a user loads the app without a pageId in the url route. This is the page that is loaded when you navigate to `yourdomain.com`. - `experimental_initPageId: string`: The pageId of the page that should be loaded when app is initialized. User is then redirected to requeted page. You can use onInit/onInitAsync/onEnter/onEnterAsync events to fetch and prepare global variables for other parts of the app. diff --git a/packages/docs/deployment/docker.yaml b/packages/docs/deployment/docker.yaml index 088b5c25a..b56bf82f2 100644 --- a/packages/docs/deployment/docker.yaml +++ b/packages/docs/deployment/docker.yaml @@ -32,7 +32,7 @@ _ref: The Lowdefy server can be configured using the following environment variables: - - `LOWDEFY_SERVER_BASE_PATH`: Set the base path to serve the Lowdefy application from. This will serve the application under `https://example.com/`instead of `https://example.com`, and all pages under `https://example.com//` instead of the default `https://example.com/`. + - `LOWDEFY_BASE_PATH`: Set the base path to serve the Lowdefy application from. This will serve the application under `https://example.com/`instead of `https://example.com`, and all pages under `https://example.com//` instead of the default `https://example.com/`. - `LOWDEFY_SERVER_BUILD_DIRECTORY`: The directory of the built Lowdefy configuration (The output of `lowdefy build`, usually found at `./.lowdefy/build` in your project repository). The default is `./build` (or `/home/node/lowdefy/build`). - `LOWDEFY_SERVER_PUBLIC_DIRECTORY`: The directory of the public assets to be served. The default is `./public` (or `/home/node/lowdefy/public`). - `LOWDEFY_SERVER_PORT`: The port (inside the container) at which to run the server. The default is `3000`. diff --git a/packages/docs/deployment/node-server.yaml b/packages/docs/deployment/node-server.yaml index c958f8205..00ccf64dc 100644 --- a/packages/docs/deployment/node-server.yaml +++ b/packages/docs/deployment/node-server.yaml @@ -105,7 +105,7 @@ _ref: The following environment variables can be specified: - - `LOWDEFY_SERVER_BASE_PATH`: Set the base path to serve the Lowdefy application from. This will serve the application under `https://example.com/`instead of `https://example.com`, and all pages under `https://example.com//` instead of the default `https://example.com/`. + - `LOWDEFY_BASE_PATH`: Set the base path to serve the Lowdefy application from. This will serve the application under `https://example.com/`instead of `https://example.com`, and all pages under `https://example.com//` instead of the default `https://example.com/`. - `LOWDEFY_SERVER_BUILD_DIRECTORY`: The directory of the built Lowdefy configuration (The output of `lowdefy build`, usually found at `./.lowdefy/build` in your project repository). The default is `./.lowdefy/build`. - `LOWDEFY_SERVER_PORT`: The port at which to run the server. The default is `3000`. - `LOWDEFY_SERVER_PUBLIC_DIRECTORY`: The directory of the public assets to be served. The default is `./public`. diff --git a/packages/server-dev/next.config.js b/packages/server-dev/next.config.js index 9f34f921f..82f9b1a43 100644 --- a/packages/server-dev/next.config.js +++ b/packages/server-dev/next.config.js @@ -7,6 +7,7 @@ module.exports = withLess({ modifyVars: lowdefyConfig.theme.lessVariables, }, }, + basePath: process.env.LOWDEFY_BASE_PATH || lowdefyConfig.basePath, // reactStrictMode: true, webpack: (config, { isServer }) => { if (!isServer) { diff --git a/packages/server/next.config.js b/packages/server/next.config.js index bfa151754..3f2aaa47f 100644 --- a/packages/server/next.config.js +++ b/packages/server/next.config.js @@ -1,7 +1,9 @@ const withLess = require('next-with-less'); const lowdefyConfig = require('./build/config.json'); +// TODO: Trance env and args from cli that is required on the server. module.exports = withLess({ + basePath: process.env.LOWDEFY_BASE_PATH || lowdefyConfig.basePath, lessLoaderOptions: { lessOptions: { modifyVars: lowdefyConfig.theme.lessVariables, diff --git a/packages/server/src/components/LowdefyContext.js b/packages/server/src/components/LowdefyContext.js index 9b1402503..a566528e1 100644 --- a/packages/server/src/components/LowdefyContext.js +++ b/packages/server/src/components/LowdefyContext.js @@ -25,6 +25,7 @@ const LowdefyContext = ({ children, lowdefy }) => { lowdefy._internal = { blockComponents, callRequest, + components: {}, document, operators, updaters: {}, diff --git a/packages/server/src/components/Page.js b/packages/server/src/components/Page.js index f8347d76a..9deb754e0 100644 --- a/packages/server/src/components/Page.js +++ b/packages/server/src/components/Page.js @@ -32,10 +32,13 @@ const Page = ({ lowdefy, pageConfig, rootConfig }) => { lowdefy._internal.router = router; lowdefy._internal.link = setupLink(lowdefy); lowdefy._internal.components = createComponents(lowdefy); + + lowdefy.basePath = lowdefy._internal.router.basePath; lowdefy.home = rootConfig.home; lowdefy.lowdefyGlobal = rootConfig.lowdefyGlobal; lowdefy.menus = rootConfig.menus; lowdefy.urlQuery = urlQuery.parse(window.location.search.slice(1)); + return ( {(context, loading) => { diff --git a/packages/server/src/components/block/CategorySwitch.js b/packages/server/src/components/block/CategorySwitch.js index a68bfeb53..a700c62d6 100644 --- a/packages/server/src/components/block/CategorySwitch.js +++ b/packages/server/src/components/block/CategorySwitch.js @@ -64,12 +64,10 @@ const CategorySwitch = ({ block, Blocks, context, lowdefy }) => { setValue: block.setValue, triggerEvent: block.triggerEvent, })} - // TODO: React throws a basePath warning - basePath={lowdefy._internal.basePath} + basePath={lowdefy.basePath} blockId={block.blockId} components={lowdefy._internal.components} events={block.eval.events} - homePageId={lowdefy.home.pageId} key={block.blockId} loading={block.loading} menus={lowdefy.menus} @@ -98,11 +96,10 @@ const CategorySwitch = ({ block, Blocks, context, lowdefy }) => { registerMethod: block.registerMethod, triggerEvent: block.triggerEvent, })} - basePath={lowdefy._internal.basePath} + basePath={lowdefy.basePath} blockId={block.blockId} components={lowdefy._internal.components} events={block.eval.events} - homePageId={lowdefy.home.pageId} key={block.blockId} loading={block.loading} menus={lowdefy.menus} diff --git a/packages/server/src/components/block/Container.js b/packages/server/src/components/block/Container.js index 773761c25..8c8f5e840 100644 --- a/packages/server/src/components/block/Container.js +++ b/packages/server/src/components/block/Container.js @@ -65,12 +65,11 @@ const Container = ({ block, Blocks, Component, context, lowdefy }) => { registerMethod: block.registerMethod, triggerEvent: block.triggerEvent, })} - basePath={lowdefy._internal.basePath} + basePath={lowdefy.basePath} blockId={block.blockId} components={lowdefy._internal.components} content={content} events={block.eval.events} - homePageId={lowdefy.home.pageId} key={block.blockId} loading={block.loading} menus={lowdefy.menus} diff --git a/packages/server/src/components/block/List.js b/packages/server/src/components/block/List.js index db46e5b5a..bd3db80e2 100644 --- a/packages/server/src/components/block/List.js +++ b/packages/server/src/components/block/List.js @@ -72,11 +72,10 @@ const List = ({ block, Blocks, Component, context, lowdefy }) => { triggerEvent: block.triggerEvent, unshiftItem: block.unshiftItem, })} - basePath={lowdefy._internal.basePath} + basePath={lowdefy.basePath} blockId={block.blockId} components={lowdefy._internal.components} events={block.eval.events} - homePageId={lowdefy.home.pageId} key={block.blockId} list={contentList} loading={block.loading} diff --git a/packages/server/src/pages/index.js b/packages/server/src/pages/index.js index 5d34caf7b..18395368e 100644 --- a/packages/server/src/pages/index.js +++ b/packages/server/src/pages/index.js @@ -19,6 +19,7 @@ import { createApiContext, getPageConfig, getRootConfig } from '@lowdefy/api'; import Page from '../components/Page.js'; export async function getServerSideProps() { + // TODO: is this build directory configurable from the cli? const apiContext = await createApiContext({ buildDirectory: './build' }); const rootConfig = await getRootConfig(apiContext); const { home } = rootConfig; diff --git a/packages/utils/block-utils/src/createIcon.js b/packages/utils/block-utils/src/createIcon.js index a85589893..d735f7177 100644 --- a/packages/utils/block-utils/src/createIcon.js +++ b/packages/utils/block-utils/src/createIcon.js @@ -30,7 +30,6 @@ const lowdefyProps = [ 'components', 'content', 'eventLog', - 'homePageId', 'list', 'loading', 'menus', diff --git a/packages/utils/node-utils/src/getConfigFromEnv.js b/packages/utils/node-utils/src/getConfigFromEnv.js index 56014246d..1cee6d3da 100644 --- a/packages/utils/node-utils/src/getConfigFromEnv.js +++ b/packages/utils/node-utils/src/getConfigFromEnv.js @@ -20,7 +20,7 @@ function getConfigFromEnv() { logLevel: process.env.LOWDEFY_SERVER_LOG_LEVEL, publicDirectory: process.env.LOWDEFY_SERVER_PUBLIC_DIRECTORY, port: process.env.LOWDEFY_SERVER_PORT && parseInt(process.env.LOWDEFY_SERVER_PORT), - serverBasePath: process.env.LOWDEFY_SERVER_BASE_PATH, + basePath: process.env.LOWDEFY_BASE_PATH, }; } diff --git a/packages/utils/node-utils/src/getConfigFromEnv.test.js b/packages/utils/node-utils/src/getConfigFromEnv.test.js index 1d2aa439e..4dd41c09e 100644 --- a/packages/utils/node-utils/src/getConfigFromEnv.test.js +++ b/packages/utils/node-utils/src/getConfigFromEnv.test.js @@ -37,7 +37,7 @@ test('Get config from env', () => { buildDirectory: 'build', publicDirectory: 'public', port: 8080, - serverBasePath: 'base', + basePath: 'base', }); }); From 9d6ac04b09f7c2281ebc699de504bba8b8b5e13b Mon Sep 17 00:00:00 2001 From: Gervwyk Date: Tue, 8 Feb 2022 15:20:16 +0200 Subject: [PATCH 12/21] feat(server): Add replace and scroll to Link. --- packages/docs/concepts/custom-blocks.yaml | 8 +++++--- .../server/src/components/createLinkComponent.js | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/docs/concepts/custom-blocks.yaml b/packages/docs/concepts/custom-blocks.yaml index 8f78ee1f0..f13e1c0a4 100644 --- a/packages/docs/concepts/custom-blocks.yaml +++ b/packages/docs/concepts/custom-blocks.yaml @@ -128,10 +128,12 @@ _ref: - `Link`: component`: Lowdefy standard Link React component used as links to pages or external urls. The following props apply: - `back: boolean`: When the link is clicked, trigger the browser back. - `home: boolean`: When the link is clicked, route to the home page. - - `pageId: string`: When the link is clicked, route to the provided Lowdefy page. - - `url: string`: When the link is clicked, route to an external url. + - `input: object`: When the link is clicked, pass data as the input object to the next Lowdefy page. Can only be used with pageId link and newTab false. See [Input]( TODO: Link to input page? ). - `newTab: boolean`: When the link is clicked, open the page in a new browser tab. - - `input: object`: When the link is clicked, pass data as the input object to the next Lowdefy page. See [Input]( TODO: Link to input page? ). + - `pageId: string`: When the link is clicked, route to the provided Lowdefy page. + - `replace: boolean`: Prevent adding a new entry into browser history by replacing the url instead of pushing into history. Can only be used with pageId link and newTab false. + - `scroll: boolean`: Disable scrolling to the top of the page after page transition. Can only be used with pageId link and newTab false. + - `url: string`: When the link is clicked, route to an external url. - `urlQuery: object`: When the link is clicked, pass data as a url query to the next Lowdefy page. See [url query]( TODO: Link to url query page? ). - `content: object`: Passed to `container` and `context` block categories. The `content` object with methods to render sub blocks into content areas. The method name is the same as the area key, for example, 'content.content()` renders a blocks default `content` area. - `events: object`: All events defined on the block in the Lowdefy app config. diff --git a/packages/server/src/components/createLinkComponent.js b/packages/server/src/components/createLinkComponent.js index f82679316..bd4c2bf33 100644 --- a/packages/server/src/components/createLinkComponent.js +++ b/packages/server/src/components/createLinkComponent.js @@ -22,7 +22,17 @@ const createLinkComponent = (lowdefy) => { ); }; - const sameOriginLink = ({ children, className, href, id, newTab, pageId, url }) => { + const sameOriginLink = ({ + children, + className, + href, + id, + newTab, + pageId, + replace, + scroll, + url, + }) => { if (newTab) { return ( { ); } return ( - + {type.isFunction(children) ? children(pageId || url || id) : children} From 80b047fb8e5684d032026d9e10b50114a67af89f Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 8 Feb 2022 16:16:19 +0200 Subject: [PATCH 13/21] fix(deps): Downgrade dependency swr to v1.1.2. https://github.com/vercel/swr/issues/1822 --- .pnp.cjs | 14 +++++++------- .../swr-npm-1.1.2-8ec1c9a7ac-84bf222c66.zip | Bin 0 -> 57970 bytes .../swr-npm-1.2.0-1529e39a5c-9858cbd184.zip | Bin 77209 -> 0 bytes packages/server-dev/package.json | 2 +- yarn.lock | 10 +++++----- 5 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 .yarn/cache/swr-npm-1.1.2-8ec1c9a7ac-84bf222c66.zip delete mode 100644 .yarn/cache/swr-npm-1.2.0-1529e39a5c-9858cbd184.zip diff --git a/.pnp.cjs b/.pnp.cjs index 9710b0259..2c6438e31 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -3549,7 +3549,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["react", "npm:18.0.0-rc.0"], ["react-dom", "virtual:573fe255dffc9c89f4f7aa60da718603753ee98acc55d6772bbd0ebdcf07f9183fb8e54b4f3f2246c538a14ead402db8d2e076039c667d1538702638a0cc87b8#npm:18.0.0-rc.0"], ["react-icons", "virtual:003bebd8b7a948d12b44e2c11a621884feb1891eea3645171e827971487f79396db9f7422bc411ccf3f90877e94ec86f5c3da70b96efb5daddb2ee3b35eae5c6#npm:4.3.1"], - ["swr", "virtual:b951ea20ab6cada5f665e8389a50d828047e6b6f10e6ebaddde1e74a94868ec6ec703ff140742f295ef663cf92da1bc80fe9bbeaab30196cba0e992f38cd19ea#npm:1.2.0"], + ["swr", "virtual:b951ea20ab6cada5f665e8389a50d828047e6b6f10e6ebaddde1e74a94868ec6ec703ff140742f295ef663cf92da1bc80fe9bbeaab30196cba0e992f38cd19ea#npm:1.1.2"], ["yargs", "npm:17.3.1"] ], "linkType": "SOFT", @@ -17816,17 +17816,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }] ]], ["swr", [ - ["npm:1.2.0", { - "packageLocation": "./.yarn/cache/swr-npm-1.2.0-1529e39a5c-9858cbd184.zip/node_modules/swr/", + ["npm:1.1.2", { + "packageLocation": "./.yarn/cache/swr-npm-1.1.2-8ec1c9a7ac-84bf222c66.zip/node_modules/swr/", "packageDependencies": [ - ["swr", "npm:1.2.0"] + ["swr", "npm:1.1.2"] ], "linkType": "SOFT", }], - ["virtual:b951ea20ab6cada5f665e8389a50d828047e6b6f10e6ebaddde1e74a94868ec6ec703ff140742f295ef663cf92da1bc80fe9bbeaab30196cba0e992f38cd19ea#npm:1.2.0", { - "packageLocation": "./.yarn/__virtual__/swr-virtual-7bd309a097/0/cache/swr-npm-1.2.0-1529e39a5c-9858cbd184.zip/node_modules/swr/", + ["virtual:b951ea20ab6cada5f665e8389a50d828047e6b6f10e6ebaddde1e74a94868ec6ec703ff140742f295ef663cf92da1bc80fe9bbeaab30196cba0e992f38cd19ea#npm:1.1.2", { + "packageLocation": "./.yarn/__virtual__/swr-virtual-9bbb3471f0/0/cache/swr-npm-1.1.2-8ec1c9a7ac-84bf222c66.zip/node_modules/swr/", "packageDependencies": [ - ["swr", "virtual:b951ea20ab6cada5f665e8389a50d828047e6b6f10e6ebaddde1e74a94868ec6ec703ff140742f295ef663cf92da1bc80fe9bbeaab30196cba0e992f38cd19ea#npm:1.2.0"], + ["swr", "virtual:b951ea20ab6cada5f665e8389a50d828047e6b6f10e6ebaddde1e74a94868ec6ec703ff140742f295ef663cf92da1bc80fe9bbeaab30196cba0e992f38cd19ea#npm:1.1.2"], ["@types/react", null], ["react", "npm:18.0.0-rc.0"] ], diff --git a/.yarn/cache/swr-npm-1.1.2-8ec1c9a7ac-84bf222c66.zip b/.yarn/cache/swr-npm-1.1.2-8ec1c9a7ac-84bf222c66.zip new file mode 100644 index 0000000000000000000000000000000000000000..d19ff9393710b7809dc097b21d31242b22313ab3 GIT binary patch literal 57970 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 literal 0 HcmV?d00001 diff --git a/.yarn/cache/swr-npm-1.2.0-1529e39a5c-9858cbd184.zip b/.yarn/cache/swr-npm-1.2.0-1529e39a5c-9858cbd184.zip deleted file mode 100644 index 4605ea246b8b600b55ff424d59602493a0a9cffc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77209 zcma&NQ;aS^w=LSXZQI6bcdxc>+qP}nwr$(Ct=0J2?)%@9^Kf#q_rAB1!owJqR5EMS z%&ZIrX;3gUp#QfC)~!MPuf_i!;Quq**_)W^+uEDB+L$^sD*U%lXq`7~{2B!aO^$6S?Ab zD=0d;S3o?&CL-B7k`$P;ZMc&ZvHoTde^)l}!1-z*;R%lk_+94Uf~UQ_KG!&Ru6r<) zKX4BD`4+DctAeYZu@Eo+3v3VpR0JZt@4V*j_6)npuPAhEI|@XVp;y39<_KgvzZ{wT zCgzg2&PMIac3T1+XaCdi9y4m~qtQ8q%Ex@hpnCINz-`Wn@9Kh7t5>T4G-t2g%R00V z8)9Q~dq@Fuefta5oa7w|a9@Xga8j;$_nU!O(T}1~UApN&FSw>)`Ao-}I2g44rbpJ0 zXjZxg;UN6DWFtf$n6CXsJWU4-XkIF+6SAa;rb!2}ZjyO)rI@Dl_^VTSpHB;GnXp1d zG>Dsu#-4~STu$rrAf74REW4qUmBf`U1sWb@O}gwl$F5?nTd7sx6V_5nmyV@yND4uJE93KT*{dy35>z~lANIm zrOR5}SjD$d3z8}s2c+XvN<}4BEm`qc`5Pk_xQR#PyE`r!K+_yhmlDpru)vbSaE#(sKq4eY9G_r6!X8UCOCR zn+6>^NfyQ!p*$_40PPTt1&OX_RGJ8lYJNvzae< z+7WX<-wyC?qj?T?WH?qLFtwx>0C>ONMWg6liG6qKOr+@NOjC2loj)q_{zwSKom#Me zvgg&8cpxqFYDdsta51#MXxHYR&yPF`JnXC27Tb8{Q#NNSwT$Bf7_iSZ6J@0xQ%)Q1 zJ7k(#K2@))*Ss?slZpcJi)I}18i+1%B1JhQ%kKBNa{rlhW{`oAaJsz7ke{L8xI_WjP>~}V`7@uGy3!J z-bE+F0)D>!ttTJ@_}l95Ma{5Bgl4WL@I1PmJehZCE zbmaScadK}9hRT0Os4J%5Dkry^50ZtKo2&QqyAKHj5jz4n>>kc{MA=dQ{koYwJRQY` z^JJLbC)JNnYk&+q-LdVcvAboO;vgeZciM_{BsfG_h(5)|i~vBN$0j`VYP@o@4F zO?$W^{XCp2bopoAUf%CEpWwOhSiP3j%FojcfnGqTf*_HLSDJ_- z4#iQ6uaCwQ+CuF zAjS7DQe`jl)fTiccbX)7zv}j>JgA+K;QrV1FE}<&`+l_l59;r$U=O5T0$S5@6TR+RKG0h>hcrB zAw+zvTT;=^Tsula1&MiOWi9wGax=3s=f4L@QEam{u0p# z>J_QMp(EAolK^_o!bC?*erL*OW$)&%@J*ptQ@DIbr{7bP2&fJgUUVQ8ef}^kZwK~WGEUtzX|7BEns40SopVwr z>ZM>{8tv)d69iC5>GV%@d*-6U{No5m1*kG{_ zvghWAx`WcIM18&@*e;+1=mZ!n`n*HPqavsV-O#eCN)i zr_z*ChyMq6oCyBk%Ui!U-yqfoCh5S{_;=XL{>w;#wD{2|tjI}zCg8<`FLB^>Kq&Hj z;jj>T$(^9YTTFleL+_B`iygv}K~lX5dPLuA$73Osbye4FnnPi;AX;4ZR}0jLq@0#=A`-%IMXTna&W>a46-rs;EzlzaF}X8Kp6OaJ zop>M}J%F+P^_jme3ikvW7nnlg_Gi6)H-ak2F{0&O>EPIk5zigz7sqtzn;*piFS5iP zz@JM&4K$#Q%`I0+@CheRcyYð}w}JQRTphyy*IPl9%W;CC|ZYobID^RQ^WCy{2& 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

*x4aCaxtAN9fR~{J>uey#90ZIp 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 Date: Tue, 8 Feb 2022 17:31:24 +0200 Subject: [PATCH 14/21] fix(operators-js): Update shared operators tests. --- .../src/operators/shared/and.test.js | 61 +++++-- .../src/operators/shared/date.test.js | 8 +- .../src/operators/shared/eq.test.js | 5 + .../src/operators/shared/if.test.js | 141 +++----------- .../src/operators/shared/log.test.js | 93 ++-------- .../src/operators/shared/not.test.js | 52 ++---- .../src/operators/shared/number.test.js | 2 +- .../src/operators/shared/or.test.js | 121 ++++++------ .../src/operators/shared/regex.test.js | 172 +++++++----------- .../src/operators/shared/string.test.js | 160 ++++++++-------- 10 files changed, 320 insertions(+), 495 deletions(-) diff --git a/packages/plugins/operators/operators-js/src/operators/shared/and.test.js b/packages/plugins/operators/operators-js/src/operators/shared/and.test.js index dbcc34722..1affc7945 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/and.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/and.test.js @@ -13,34 +13,69 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { NodeParser, WebParser } from '@lowdefy/operators'; +import _and from './and.js'; -import and from './and.js'; +const operators = { + _and, +}; const location = 'location'; test('_and false', () => { - expect(and({ params: [0, 0], location })).toEqual(false); - expect(and({ params: [0, 1], location })).toEqual(false); - expect(and({ params: [1, 2, 3, 0], location })).toEqual(false); - expect(and({ params: [false, false], location })).toEqual(false); - expect(and({ params: [false, true], location })).toEqual(false); + expect(_and({ params: [0, 0], location })).toEqual(false); + expect(_and({ params: [0, 1], location })).toEqual(false); + expect(_and({ params: [1, 2, 3, 0], location })).toEqual(false); + expect(_and({ params: [false, false], location })).toEqual(false); + expect(_and({ params: [false, true], location })).toEqual(false); }); test('_and true', () => { - expect(and({ params: [1, 2], location })).toEqual(true); - expect(and({ params: [1, 2, 3], location })).toEqual(true); - expect(and({ params: [true, true], location })).toEqual(true); + expect(_and({ params: [1, 2], location })).toEqual(true); + expect(_and({ params: [1, 2, 3], location })).toEqual(true); + expect(_and({ params: [true, true], location })).toEqual(true); }); test('_and errors', () => { - expect(() => and({ params: 'hello', location })).toThrow( + expect(() => _and({ params: 'hello', location })).toThrow( 'Operator Error: _and takes an array type. Received: "hello" at location.' ); - expect(() => and({ params: null, location })).toThrow( + expect(() => _and({ params: null, location })).toThrow( 'Operator Error: _and takes an array type. Received: null at location.' ); - expect(() => and({ params: true, location })).toThrow( + expect(() => _and({ params: true, location })).toThrow( 'Operator Error: _and takes an array type. Received: true at location.' ); - expect(() => and({ params: false, location })).toThrow( + expect(() => _and({ params: false, location })).toThrow( 'Operator Error: _and takes an array type. Received: false at location.' ); }); + +test('_and calls NodeParser', async () => { + const input = { a: { _and: [true, true] } }; + const parser = new NodeParser({ operators, payload: {}, secrets: {}, user: {} }); + await parser.init(); + const res = parser.parse({ input, location }); + expect(res.output).toEqual({ a: true }); +}); + +test('_and calls WebParser', async () => { + const context = { + _internal: { + lowdefy: { + inputs: { id: true }, + lowdefyGlobal: { global: true }, + menus: [{ menus: true }], + urlQuery: { urlQuery: true }, + user: { user: true }, + }, + }, + eventLog: [{ eventLog: true }], + id: 'id', + requests: [{ requests: true }], + state: { state: true }, + }; + const input = { a: { _and: [true, true] } }; + const parser = new WebParser({ context, operators }); + await parser.init(); + const res = parser.parse({ input, location }); + expect(res.output).toEqual({ a: true }); +}); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/date.test.js b/packages/plugins/operators/operators-js/src/operators/shared/date.test.js index 93f69b89d..82122c9ae 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/date.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/date.test.js @@ -16,7 +16,7 @@ import date from './date.js'; -const location = 'locationId'; +const location = 'location'; test('_date now', () => { const RealDate = Date; @@ -57,19 +57,19 @@ test('_date negative int', () => { test('_date null', () => { expect(() => date({ params: null, location })).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _date.__default accepts one of the following types: number, string. - Received: {\\"_date.__default\\":null} at locationId." + Received: {\\"_date.__default\\":null} at location." `); }); test('_date invalid operator type', () => { expect(() => date({ params: {}, location })).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _date.__default accepts one of the following types: number, string. - Received: {\\"_date.__default\\":{}} at locationId." + Received: {\\"_date.__default\\":{}} at location." `); }); test('_date invalid string', () => { expect(() => date({ params: 'abc', location })).toThrowErrorMatchingInlineSnapshot( - `"Operator Error: _date.__default - abc could not resolve as a valid javascript date. Received: {\\"_date.__default\\":\\"abc\\"} at locationId."` + `"Operator Error: _date.__default - abc could not resolve as a valid javascript date. Received: {\\"_date.__default\\":\\"abc\\"} at location."` ); }); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/eq.test.js b/packages/plugins/operators/operators-js/src/operators/shared/eq.test.js index 1b547282a..21cfa1ac5 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/eq.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/eq.test.js @@ -16,6 +16,8 @@ import eq from './eq.js'; +const location = 'location'; + test('_eq false', () => { expect(eq({ params: [1, 2], location })).toEqual(false); expect(eq({ params: [0, 1], location })).toEqual(false); @@ -41,4 +43,7 @@ test('_eq errors', () => { expect(() => eq({ params: false, location })).toThrow( 'Operator Error: _eq takes an array type as input. Received: false at location.' ); + expect(() => eq({ params: [1, 2, 3], location })).toThrow( + 'Operator Error: _eq takes an array of length 2 as input. Received: [1,2,3] at location.' + ); }); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/if.test.js b/packages/plugins/operators/operators-js/src/operators/shared/if.test.js index 41e6ab0ef..b94fe0864 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/if.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/if.test.js @@ -13,122 +13,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { NodeParser } from '@lowdefy/operators'; +// import { NodeParser } from '@lowdefy/operators'; + +import _if from './if.js'; + +const location = 'location'; console.error = () => {}; -test('_if', async () => { - const parser = new NodeParser(); - await parser.init(); - let res = parser.parse({ - input: { - _if: { - test: true, - then: 1, - else: 2, - }, - }, - location: 'locationId', - }); - expect(res.output).toBe(1); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ - input: { - _if: { - test: false, - then: 1, - else: 2, - }, - }, - location: 'locationId', - }); - expect(res.output).toBe(2); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ - input: { - _if: { - then: 1, - else: 2, - }, - }, - location: 'locationId', - }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _if takes a boolean type for parameter test. Received: {"then":1,"else":2} at locationId.], - ] - `); - res = parser.parse({ - input: { - _if: { - test: false, - then: 1, - }, - }, - location: 'locationId', - }); - expect(res.output).toBe(undefined); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ - input: { - _if: { - test: true, - else: 2, - }, - }, - location: 'locationId', - }); - expect(res.output).toBe(undefined); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ - input: { - _if: { - test: { - a: [1, 3], - }, - then: 1, - else: 2, - }, - }, - location: 'locationId', - }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _if takes a boolean type for parameter test. Received: {"test":{"a":[1,3]},"then":1,"else":2} at locationId.], - ] - `); - res = parser.parse({ - input: { - _if: { - test: 'True', - then: 1, - else: 2, - }, - }, - location: 'locationId', - }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _if takes a boolean type for parameter test. Received: {"test":"True","then":1,"else":2} at locationId.], - ] - `); - res = parser.parse({ - input: { - _if: { - test: 1, - then: 1, - else: 2, - }, - }, - location: 'locationId', - }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _if takes a boolean type for parameter test. Received: {"test":1,"then":1,"else":2} at locationId.], - ] - `); +test('_if then', () => { + expect(_if({ params: { test: true, then: 1, else: 2 }, location })).toEqual(1); + expect(_if({ params: { test: true, else: 2 }, location })).toEqual(undefined); +}); +test('_if else', () => { + expect(_if({ params: { test: false, then: 1, else: 2 }, location })).toEqual(2); + expect(_if({ params: { test: false, then: 1 }, location })).toEqual(undefined); +}); +test('_if errors', () => { + expect(() => _if({ params: { then: 1, else: 2 }, location })).toThrow( + 'Operator Error: _if takes a boolean type for parameter test. Received: {"then":1,"else":2} at location.' + ); + expect(() => _if({ params: { test: { a: [1, 3] }, then: 1, else: 2 }, location })).toThrow( + 'Operator Error: _if takes a boolean type for parameter test. Received: {"test":{"a":[1,3]},"then":1,"else":2} at location.' + ); + expect(() => _if({ params: { test: 'True', then: 1, else: 2 }, location })).toThrow( + 'Operator Error: _if takes a boolean type for parameter test. Received: {"test":"True","then":1,"else":2} at location.' + ); + expect(() => _if({ params: { test: 1, then: 1, else: 2 }, location })).toThrow( + 'Operator Error: _if takes a boolean type for parameter test. Received: {"test":1,"then":1,"else":2} at location.' + ); }); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/log.test.js b/packages/plugins/operators/operators-js/src/operators/shared/log.test.js index 2d600e71f..b3fcf914d 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/log.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/log.test.js @@ -15,7 +15,9 @@ */ /* eslint-disable max-classes-per-file */ -import { NodeParser } from '@lowdefy/operators'; +import _log from './log.js'; + +const location = 'location'; const logger = console.log; const mockLogger = jest.fn(); @@ -27,91 +29,36 @@ afterAll(() => { console.log = logger; }); -test('_log a string', async () => { - const input = { a: { _log: 'value' } }; - const parser = new NodeParser(); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toEqual({ - a: 'value', - }); +test('_log a string', () => { + expect(_log({ params: 'value', location })).toEqual('value'); expect(mockLogger).toHaveBeenCalledWith('value'); }); - -test('_log a number', async () => { - const input = { a: { _log: 1 } }; - const parser = new NodeParser(); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toEqual({ - a: 1, - }); +test('_log a number', () => { + expect(_log({ params: 1, location })).toEqual(1); expect(mockLogger).toHaveBeenCalledWith(1); }); - -test('_log a null', async () => { - const input = { a: { _log: null } }; - const parser = new NodeParser(); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toEqual({ - a: null, - }); +test('_log a null', () => { + expect(_log({ params: null, location })).toEqual(null); expect(mockLogger).toHaveBeenCalledWith(null); }); - // TODO: Confirm if this is expected behaviour?? -test('_log a undefined', async () => { - const input = { a: { _log: undefined } }; - const parser = new NodeParser(); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toEqual({ - a: {}, - }); - expect(mockLogger).not.toHaveBeenCalled(); +test('_log an undefined', () => { + expect(_log({ params: undefined, location })).toEqual(undefined); + expect(mockLogger).toHaveBeenCalledWith(undefined); }); - -test('_log a 0', async () => { - const input = { a: { _log: 0 } }; - const parser = new NodeParser(); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toEqual({ - a: 0, - }); +test('_log a 0', () => { + expect(_log({ params: 0, location })).toEqual(0); expect(mockLogger).toHaveBeenCalledWith(0); }); - -test('_log a false', async () => { - const input = { a: { _log: false } }; - const parser = new NodeParser(); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toEqual({ - a: false, - }); +test('_log a false', () => { + expect(_log({ params: false, location })).toEqual(false); expect(mockLogger).toHaveBeenCalledWith(false); }); - -test('_log a object', async () => { - const input = { a: { _log: { b: 1 } } }; - const parser = new NodeParser(); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toEqual({ - a: { b: 1 }, - }); +test('_log an object', () => { + expect(_log({ params: { b: 1 }, location })).toEqual({ b: 1 }); expect(mockLogger).toHaveBeenCalledWith({ b: 1 }); }); - -test('_log a array', async () => { - const input = { a: { _log: [{ b: 1 }] } }; - const parser = new NodeParser(); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toEqual({ - a: [{ b: 1 }], - }); +test('_log an array', () => { + expect(_log({ params: [{ b: 1 }], location })).toEqual([{ b: 1 }]); expect(mockLogger).toHaveBeenCalledWith([{ b: 1 }]); }); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/not.test.js b/packages/plugins/operators/operators-js/src/operators/shared/not.test.js index cb9534a43..bc4d6978a 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/not.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/not.test.js @@ -13,48 +13,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { NodeParser } from '@lowdefy/operators'; +import _not from './not.js'; -const arr0 = [0, 0]; -const arr1 = [0, 1]; -const arr2 = [1, 2]; -const arr3 = [1, 2, 3]; -const arr30 = [1, 2, 3, 0]; -const string = 'hello'; -const Null = null; -const True = true; -const False = false; +const location = 'location'; console.error = () => {}; -test('_not', async () => { - const parser = new NodeParser(); - await parser.init(); - let res = parser.parse({ input: { _not: arr0 }, location: 'locationId' }); - expect(res.output).toEqual(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _not: arr1 }, location: 'locationId' }); - expect(res.output).toEqual(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _not: arr2 }, location: 'locationId' }); - expect(res.output).toEqual(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _not: arr3 }, location: 'locationId' }); - expect(res.output).toEqual(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _not: arr30 }, location: 'locationId' }); - expect(res.output).toEqual(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _not: string }, location: 'locationId' }); - expect(res.output).toEqual(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _not: Null }, location: 'locationId' }); - expect(res.output).toEqual(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _not: True }, location: 'locationId' }); - expect(res.output).toEqual(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _not: False }, location: 'locationId' }); - expect(res.output).toEqual(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); +test('_not returns true', () => { + expect(_not({ params: 0, location })).toEqual(true); + expect(_not({ params: null, location })).toEqual(true); + expect(_not({ params: false, location })).toEqual(true); +}); +test('_not returns false', () => { + expect(_not({ params: 1, location })).toEqual(false); + expect(_not({ params: true, location })).toEqual(false); + expect(_not({ params: [0, 0], location })).toEqual(false); + expect(_not({ params: 'string', location })).toEqual(false); }); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/number.test.js b/packages/plugins/operators/operators-js/src/operators/shared/number.test.js index d86e648c9..b5d304ef6 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/number.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/number.test.js @@ -56,7 +56,7 @@ test('_number valid functions', () => { ); expect( _number({ methodName: 'toLocaleString', params: [123456.789, 'de-DE'], location: 'locationId' }) - ).toBe('123,456.789'); + ).toBe('123.456,789'); expect( _number({ methodName: 'toPrecision', params: [5.123456, 2], location: 'locationId' }) ).toBe('5.1'); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/or.test.js b/packages/plugins/operators/operators-js/src/operators/shared/or.test.js index cd724816a..3f6b8feea 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/or.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/or.test.js @@ -10,67 +10,72 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and + See the License for the specific language governing permissions or limitations under the License. */ -import { NodeParser } from '@lowdefy/operators'; +import { NodeParser, WebParser } from '@lowdefy/operators'; +import _or from './or.js'; -const arr0 = [0, 0]; -const arr1 = [0, 1]; -const arr2 = [1, 2]; -const arr3 = [1, 2, 3]; -const arr30 = [1, 2, 3, 0]; -const string = 'hello'; -const Null = null; -const True = true; -const False = false; +const operators = { + _or, +}; -console.error = () => {}; +const location = 'location'; -test('_or', async () => { - const parser = new NodeParser(); - await parser.init(); - let res = parser.parse({ input: { _or: arr0 }, location: 'locationId' }); - expect(res.output).toEqual(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _or: arr1 }, location: 'locationId' }); - expect(res.output).toEqual(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _or: arr2 }, location: 'locationId' }); - expect(res.output).toEqual(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _or: arr3 }, location: 'locationId' }); - expect(res.output).toEqual(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _or: arr30 }, location: 'locationId' }); - expect(res.output).toEqual(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); - res = parser.parse({ input: { _or: string }, location: 'locationId' }); - expect(res.output).toEqual(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _or takes an array type. Received: "hello" at locationId.], - ] - `); - res = parser.parse({ input: { _or: Null }, location: 'locationId' }); - expect(res.output).toEqual(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _or takes an array type. Received: null at locationId.], - ] - `); - res = parser.parse({ input: { _or: True }, location: 'locationId' }); - expect(res.output).toEqual(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _or takes an array type. Received: true at locationId.], - ] - `); - res = parser.parse({ input: { _or: False }, location: 'locationId' }); - expect(res.output).toEqual(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _or takes an array type. Received: false at locationId.], - ] - `); +test('_or false', () => { + expect(_or({ params: [0, 0], location })).toEqual(false); + expect(_or({ params: [false, false], location })).toEqual(false); +}); +test('_or true', () => { + expect(_or({ params: [0, 1], location })).toEqual(true); + expect(_or({ params: [1, 2], location })).toEqual(true); + expect(_or({ params: [1, 2, 3], location })).toEqual(true); + expect(_or({ params: [1, 2, 3, 0], location })).toEqual(true); + expect(_or({ params: [true, true], location })).toEqual(true); + expect(_or({ params: [false, true], location })).toEqual(true); +}); +test('_or errors', () => { + expect(() => _or({ params: 'hello', location })).toThrow( + 'Operator Error: _or takes an array type. Received: "hello" at location.' + ); + expect(() => _or({ params: null, location })).toThrow( + 'Operator Error: _or takes an array type. Received: null at location.' + ); + expect(() => _or({ params: true, location })).toThrow( + 'Operator Error: _or takes an array type. Received: true at location.' + ); + expect(() => _or({ params: false, location })).toThrow( + 'Operator Error: _or takes an array type. Received: false at location.' + ); +}); + +test('_or calls NodeParser', async () => { + const input = { a: { _or: [true, false] } }; + const parser = new NodeParser({ operators, payload: {}, secrets: {}, user: {} }); + await parser.init(); + const res = parser.parse({ input, location }); + expect(res.output).toEqual({ a: true }); +}); + +test('_or calls WebParser', async () => { + const context = { + _internal: { + lowdefy: { + inputs: { id: true }, + lowdefyGlobal: { global: true }, + menus: [{ menus: true }], + urlQuery: { urlQuery: true }, + user: { user: true }, + }, + }, + eventLog: [{ eventLog: true }], + id: 'id', + requests: [{ requests: true }], + state: { state: true }, + }; + const input = { a: { _or: [true, false] } }; + const parser = new WebParser({ context, operators }); + await parser.init(); + const res = parser.parse({ input, location }); + expect(res.output).toEqual({ a: true }); }); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/regex.test.js b/packages/plugins/operators/operators-js/src/operators/shared/regex.test.js index 9b2139c18..e494039c8 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/regex.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/regex.test.js @@ -15,127 +15,77 @@ */ import { NodeParser } from '@lowdefy/operators'; +import _regex from './regex.js'; -const state = { - string: 'Some String', - number: 42, - arr: [{ a: 'a1' }, { a: 'a2' }], -}; +const location = 'location'; console.error = () => {}; -test('_regex with on, pass', async () => { - const input = { _regex: { pattern: '^a$', on: 'a' } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); +test('_regex with on, pass', () => { + expect(_regex({ params: { on: 'a', pattern: '^a$' }, location })).toEqual(true); }); - -test('_regex with on, fail', async () => { - const input = { _regex: { pattern: '^a$', on: 'b' } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); +test('_regex with on, fail', () => { + expect(_regex({ params: { on: 'b', pattern: '^a$' }, location })).toEqual(false); }); - -// NOTE: key not supported by NodeParser -// test('_regex with key, pass', async () => { -// const input = { _regex: { pattern: '^Some String$', key: 'string' } }; -// const parser = new NodeParser({ state }); -// await parser.init(); -// const res = parser.parse({ input, location: 'locationId' }); -// expect(res.output).toBe(true); -// expect(res.errors).toMatchInlineSnapshot(`Array []`); -// }); - -// test('_regex with key, fail', async () => { -// const input = { _regex: { pattern: '^a$', key: 'string' } }; -// const parser = new NodeParser({ state }); -// await parser.init(); -// const res = parser.parse({ input, location: 'locationId' }); -// expect(res.output).toBe(false); -// expect(res.errors).toMatchInlineSnapshot(`Array []`); -// }); - -test('_regex with null on', async () => { - const input = { _regex: { pattern: '^a$', on: null } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); +test('_regex with null on', () => { + expect(_regex({ params: { on: null, pattern: '^a$' }, location })).toEqual(false); }); - -test('_regex with nonexistent key', async () => { - const input = { _regex: { pattern: '^a$', key: 'notThere' } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); +test('_regex with on, pass', () => { + expect(_regex({ params: { on: 'a', pattern: '^a$' }, location })).toEqual(true); }); - -test('_regex with nonexistent key', async () => { - const input = { _regex: { pattern: '^a$', key: null } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _regex.key must be a string. Received: {"pattern":"^a$","key":null} at locationId.], - ] - `); +test('_regex with key, pass', () => { + expect( + _regex({ + params: { key: 'string', pattern: '^Some String$' }, + location, + state: { string: 'Some String' }, + }) + ).toEqual(true); }); - -test('_regex null', async () => { - const input = { _regex: null }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _regex.pattern must be a string. Received: null at locationId.], - ] - `); +test('_regex with key, fail', () => { + expect( + _regex({ + params: { key: 'string', pattern: '^a$' }, + location, + state: { string: 'Some String' }, + }) + ).toEqual(false); }); - -test('_regex with non-string on', async () => { - const input = { _regex: { pattern: '^a$', on: 5 } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _regex.on must be a string. Received: {"pattern":"^a$","on":5} at locationId.], - ] - `); +test('_regex with nonexistent', () => { + expect( + _regex({ + params: { key: 'notThere', pattern: '^a$' }, + location, + state: { string: 'Some String' }, + }) + ).toEqual(false); }); - -test('_regex flags', async () => { - const input = { _regex: { pattern: 'a', on: 'A', flags: 'i' } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); +test('_regex with null key', () => { + expect(() => + _regex({ + params: { key: null, pattern: '^a$' }, + location, + state: { string: 'Some String' }, + }) + ).toThrow( + 'Operator Error: _regex.key must be a string. Received: {"key":null,"pattern":"^a$"} at location.' + ); }); - -test('_regex invalid flags', async () => { - const input = { _regex: { pattern: 'a', on: 'a', flags: 1 } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _regex failed to execute RegExp.test. Received: {"pattern":"a","on":"a","flags":1} at locationId.], - ] - `); +test('_regex null', () => { + expect(() => _regex({ params: null, location })).toThrow( + 'Operator Error: _regex.pattern must be a string. Received: null at location.' + ); +}); +test('_regex with non-string on', () => { + expect(() => _regex({ params: { pattern: '^a$', on: 5 }, location })).toThrow( + 'Operator Error: _regex.on must be a string. Received: {"pattern":"^a$","on":5} at location.' + ); +}); +test('_regex flags', () => { + expect(_regex({ params: { on: 'A', pattern: '^a$', flags: 'i' }, location })).toEqual(true); +}); +test('_regex invalid flags', () => { + expect(() => _regex({ params: { pattern: '^a$', on: 'A', flags: 1 }, location })).toThrow( + 'Operator Error: _regex failed to execute RegExp.test. Received: {"pattern":"^a$","on":"A","flags":1} at location.' + ); }); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/string.test.js b/packages/plugins/operators/operators-js/src/operators/shared/string.test.js index 283fd83eb..d0435b92b 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/string.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/string.test.js @@ -15,7 +15,7 @@ */ import string from './string.js'; -const location = 'locationId'; +const location = 'location'; describe('_string.charAt', () => { const methodName = 'charAt'; @@ -51,7 +51,7 @@ describe('_string.charAt', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.charAt must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.charAt\\":[1,2]} at locationId." + Received: {\\"_string.charAt\\":[1,2]} at location." `); expect(() => string({ @@ -61,7 +61,7 @@ describe('_string.charAt', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.charAt must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.charAt\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.charAt\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -71,7 +71,7 @@ describe('_string.charAt', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.charAt accepts one of the following types: array, object. - Received: {\\"_string.charAt\\":null} at locationId." + Received: {\\"_string.charAt\\":null} at location." `); }); }); @@ -124,7 +124,7 @@ describe('_string.concat', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.concat must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.concat\\":[1,2]} at locationId." + Received: {\\"_string.concat\\":[1,2]} at location." `); expect(() => string({ @@ -134,7 +134,7 @@ describe('_string.concat', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.concat accepts one of the following types: array. - Received: {\\"_string.concat\\":\\"abc\\"} at locationId." + Received: {\\"_string.concat\\":\\"abc\\"} at location." `); expect(() => string({ @@ -144,7 +144,7 @@ describe('_string.concat', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.concat accepts one of the following types: array. - Received: {\\"_string.concat\\":null} at locationId." + Received: {\\"_string.concat\\":null} at location." `); }); }); @@ -190,7 +190,7 @@ describe('_string.endsWith', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.endsWith must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.endsWith\\":[1,2]} at locationId." + Received: {\\"_string.endsWith\\":[1,2]} at location." `); expect(() => string({ @@ -200,7 +200,7 @@ describe('_string.endsWith', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.endsWith must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.endsWith\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.endsWith\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -210,7 +210,7 @@ describe('_string.endsWith', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.endsWith accepts one of the following types: array, object. - Received: {\\"_string.endsWith\\":null} at locationId." + Received: {\\"_string.endsWith\\":null} at location." `); }); }); @@ -256,7 +256,7 @@ describe('_string.includes', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.includes must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.includes\\":[1,2]} at locationId." + Received: {\\"_string.includes\\":[1,2]} at location." `); expect(() => string({ @@ -266,7 +266,7 @@ describe('_string.includes', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.includes must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.includes\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.includes\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -276,7 +276,7 @@ describe('_string.includes', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.includes accepts one of the following types: array, object. - Received: {\\"_string.includes\\":null} at locationId." + Received: {\\"_string.includes\\":null} at location." `); }); }); @@ -329,7 +329,7 @@ describe('_string.indexOf', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.indexOf must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.indexOf\\":[1,2]} at locationId." + Received: {\\"_string.indexOf\\":[1,2]} at location." `); expect(() => string({ @@ -339,7 +339,7 @@ describe('_string.indexOf', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.indexOf must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.indexOf\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.indexOf\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -349,7 +349,7 @@ describe('_string.indexOf', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.indexOf accepts one of the following types: array, object. - Received: {\\"_string.indexOf\\":null} at locationId." + Received: {\\"_string.indexOf\\":null} at location." `); }); }); @@ -395,7 +395,7 @@ describe('_string.lastIndexOf', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.lastIndexOf must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.lastIndexOf\\":[1,2]} at locationId." + Received: {\\"_string.lastIndexOf\\":[1,2]} at location." `); expect(() => string({ @@ -405,7 +405,7 @@ describe('_string.lastIndexOf', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.lastIndexOf must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.lastIndexOf\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.lastIndexOf\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -415,7 +415,7 @@ describe('_string.lastIndexOf', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.lastIndexOf accepts one of the following types: array, object. - Received: {\\"_string.lastIndexOf\\":null} at locationId." + Received: {\\"_string.lastIndexOf\\":null} at location." `); }); }); @@ -485,7 +485,7 @@ describe('_string.match', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.match must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.match\\":[1,2]} at locationId." + Received: {\\"_string.match\\":[1,2]} at location." `); expect(() => string({ @@ -495,7 +495,7 @@ describe('_string.match', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.match must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.match\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.match\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -505,7 +505,7 @@ describe('_string.match', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.match accepts one of the following types: array, object. - Received: {\\"_string.match\\":null} at locationId." + Received: {\\"_string.match\\":null} at location." `); }); }); @@ -536,7 +536,7 @@ describe('_string.normalize', () => { location, }) ).toThrowErrorMatchingInlineSnapshot( - `"Operator Error: _string.normalize - The normalization form should be one of NFC, NFD, NFKC, NFKD. Received: {\\"_string.normalize\\":[\\"Amélie\\",2]} at locationId."` + `"Operator Error: _string.normalize - The normalization form should be one of NFC, NFD, NFKC, NFKD. Received: {\\"_string.normalize\\":[\\"Amélie\\",2]} at location."` ); expect(() => string({ @@ -546,7 +546,7 @@ describe('_string.normalize', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.normalize must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.normalize\\":[1,2]} at locationId." + Received: {\\"_string.normalize\\":[1,2]} at location." `); expect(() => string({ @@ -556,7 +556,7 @@ describe('_string.normalize', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.normalize must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.normalize\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.normalize\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -566,7 +566,7 @@ describe('_string.normalize', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.normalize accepts one of the following types: array, object. - Received: {\\"_string.normalize\\":null} at locationId." + Received: {\\"_string.normalize\\":null} at location." `); }); }); @@ -605,7 +605,7 @@ describe('_string.padEnd', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.padEnd must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.padEnd\\":[1,2]} at locationId." + Received: {\\"_string.padEnd\\":[1,2]} at location." `); expect(() => string({ @@ -615,7 +615,7 @@ describe('_string.padEnd', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.padEnd must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.padEnd\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.padEnd\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -625,7 +625,7 @@ describe('_string.padEnd', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.padEnd accepts one of the following types: array, object. - Received: {\\"_string.padEnd\\":null} at locationId." + Received: {\\"_string.padEnd\\":null} at location." `); }); }); @@ -664,7 +664,7 @@ describe('_string.padStart', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.padStart must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.padStart\\":[1,2]} at locationId." + Received: {\\"_string.padStart\\":[1,2]} at location." `); expect(() => string({ @@ -674,7 +674,7 @@ describe('_string.padStart', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.padStart must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.padStart\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.padStart\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -684,7 +684,7 @@ describe('_string.padStart', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.padStart accepts one of the following types: array, object. - Received: {\\"_string.padStart\\":null} at locationId." + Received: {\\"_string.padStart\\":null} at location." `); }); }); @@ -716,7 +716,7 @@ describe('_string.repeat', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.repeat must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.repeat\\":[1,2]} at locationId." + Received: {\\"_string.repeat\\":[1,2]} at location." `); expect(() => string({ @@ -726,7 +726,7 @@ describe('_string.repeat', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.repeat must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.repeat\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.repeat\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -736,7 +736,7 @@ describe('_string.repeat', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.repeat accepts one of the following types: array, object. - Received: {\\"_string.repeat\\":null} at locationId." + Received: {\\"_string.repeat\\":null} at location." `); }); }); @@ -775,7 +775,7 @@ describe('_string.replace', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.replace must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.replace\\":[1,2]} at locationId." + Received: {\\"_string.replace\\":[1,2]} at location." `); expect(() => string({ @@ -785,7 +785,7 @@ describe('_string.replace', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.replace must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.replace\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.replace\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -795,7 +795,7 @@ describe('_string.replace', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.replace accepts one of the following types: array, object. - Received: {\\"_string.replace\\":null} at locationId." + Received: {\\"_string.replace\\":null} at location." `); }); }); @@ -841,7 +841,7 @@ describe('_string.search', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.search must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.search\\":[1,2]} at locationId." + Received: {\\"_string.search\\":[1,2]} at location." `); expect(() => string({ @@ -851,7 +851,7 @@ describe('_string.search', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.search must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.search\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.search\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -861,7 +861,7 @@ describe('_string.search', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.search accepts one of the following types: array, object. - Received: {\\"_string.search\\":null} at locationId." + Received: {\\"_string.search\\":null} at location." `); }); }); @@ -907,7 +907,7 @@ describe('_string.slice', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.slice must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.slice\\":[1,2]} at locationId." + Received: {\\"_string.slice\\":[1,2]} at location." `); expect(() => string({ @@ -917,7 +917,7 @@ describe('_string.slice', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.slice must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.slice\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.slice\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -927,7 +927,7 @@ describe('_string.slice', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.slice accepts one of the following types: array, object. - Received: {\\"_string.slice\\":null} at locationId." + Received: {\\"_string.slice\\":null} at location." `); }); }); @@ -966,7 +966,7 @@ describe('_string.split', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.split must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.split\\":[1,2]} at locationId." + Received: {\\"_string.split\\":[1,2]} at location." `); expect(() => string({ @@ -976,7 +976,7 @@ describe('_string.split', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.split must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.split\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.split\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -986,7 +986,7 @@ describe('_string.split', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.split accepts one of the following types: array, object. - Received: {\\"_string.split\\":null} at locationId." + Received: {\\"_string.split\\":null} at location." `); }); }); @@ -1032,7 +1032,7 @@ describe('_string.startsWith', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.startsWith must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.startsWith\\":[1,2]} at locationId." + Received: {\\"_string.startsWith\\":[1,2]} at location." `); expect(() => string({ @@ -1042,7 +1042,7 @@ describe('_string.startsWith', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.startsWith must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.startsWith\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.startsWith\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -1052,7 +1052,7 @@ describe('_string.startsWith', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.startsWith accepts one of the following types: array, object. - Received: {\\"_string.startsWith\\":null} at locationId." + Received: {\\"_string.startsWith\\":null} at location." `); }); }); @@ -1098,7 +1098,7 @@ describe('_string.substring', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.substring must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.substring\\":[1,2]} at locationId." + Received: {\\"_string.substring\\":[1,2]} at location." `); expect(() => string({ @@ -1108,7 +1108,7 @@ describe('_string.substring', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.substring must be evaluated on an string instance. For named args provide an string instance to the \\"on\\" property, for listed args provide and string instance as the first element in the operator argument array. - Received: {\\"_string.substring\\":{\\"on\\":true}} at locationId." + Received: {\\"_string.substring\\":{\\"on\\":true}} at location." `); expect(() => string({ @@ -1118,7 +1118,7 @@ describe('_string.substring', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.substring accepts one of the following types: array, object. - Received: {\\"_string.substring\\":null} at locationId." + Received: {\\"_string.substring\\":null} at location." `); }); }); @@ -1143,7 +1143,7 @@ describe('_string.toLowerCase', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.toLowerCase accepts one of the following types: string. - Received: {\\"_string.toLowerCase\\":[1,2]} at locationId." + Received: {\\"_string.toLowerCase\\":[1,2]} at location." `); expect(() => string({ @@ -1153,7 +1153,7 @@ describe('_string.toLowerCase', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.toLowerCase accepts one of the following types: string. - Received: {\\"_string.toLowerCase\\":[\\"abc\\"]} at locationId." + Received: {\\"_string.toLowerCase\\":[\\"abc\\"]} at location." `); expect(() => string({ @@ -1163,7 +1163,7 @@ describe('_string.toLowerCase', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.toLowerCase accepts one of the following types: string. - Received: {\\"_string.toLowerCase\\":{\\"on\\":\\"abc\\"}} at locationId." + Received: {\\"_string.toLowerCase\\":{\\"on\\":\\"abc\\"}} at location." `); expect(() => string({ @@ -1173,7 +1173,7 @@ describe('_string.toLowerCase', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.toLowerCase accepts one of the following types: string. - Received: {\\"_string.toLowerCase\\":null} at locationId." + Received: {\\"_string.toLowerCase\\":null} at location." `); }); }); @@ -1198,7 +1198,7 @@ describe('_string.toUpperCase', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.toUpperCase accepts one of the following types: string. - Received: {\\"_string.toUpperCase\\":[1,2]} at locationId." + Received: {\\"_string.toUpperCase\\":[1,2]} at location." `); expect(() => string({ @@ -1208,7 +1208,7 @@ describe('_string.toUpperCase', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.toUpperCase accepts one of the following types: string. - Received: {\\"_string.toUpperCase\\":[\\"abc\\"]} at locationId." + Received: {\\"_string.toUpperCase\\":[\\"abc\\"]} at location." `); expect(() => string({ @@ -1218,7 +1218,7 @@ describe('_string.toUpperCase', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.toUpperCase accepts one of the following types: string. - Received: {\\"_string.toUpperCase\\":{\\"on\\":\\"abc\\"}} at locationId." + Received: {\\"_string.toUpperCase\\":{\\"on\\":\\"abc\\"}} at location." `); expect(() => string({ @@ -1228,7 +1228,7 @@ describe('_string.toUpperCase', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.toUpperCase accepts one of the following types: string. - Received: {\\"_string.toUpperCase\\":null} at locationId." + Received: {\\"_string.toUpperCase\\":null} at location." `); }); }); @@ -1253,7 +1253,7 @@ describe('_string.trim', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trim accepts one of the following types: string. - Received: {\\"_string.trim\\":[1,2]} at locationId." + Received: {\\"_string.trim\\":[1,2]} at location." `); expect(() => string({ @@ -1263,7 +1263,7 @@ describe('_string.trim', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trim accepts one of the following types: string. - Received: {\\"_string.trim\\":[\\"abc\\"]} at locationId." + Received: {\\"_string.trim\\":[\\"abc\\"]} at location." `); expect(() => string({ @@ -1273,7 +1273,7 @@ describe('_string.trim', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trim accepts one of the following types: string. - Received: {\\"_string.trim\\":{\\"on\\":\\"abc\\"}} at locationId." + Received: {\\"_string.trim\\":{\\"on\\":\\"abc\\"}} at location." `); expect(() => string({ @@ -1283,7 +1283,7 @@ describe('_string.trim', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trim accepts one of the following types: string. - Received: {\\"_string.trim\\":null} at locationId." + Received: {\\"_string.trim\\":null} at location." `); }); }); @@ -1308,7 +1308,7 @@ describe('_string.trimEnd', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trimEnd accepts one of the following types: string. - Received: {\\"_string.trimEnd\\":[1,2]} at locationId." + Received: {\\"_string.trimEnd\\":[1,2]} at location." `); expect(() => string({ @@ -1318,7 +1318,7 @@ describe('_string.trimEnd', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trimEnd accepts one of the following types: string. - Received: {\\"_string.trimEnd\\":[\\"abc\\"]} at locationId." + Received: {\\"_string.trimEnd\\":[\\"abc\\"]} at location." `); expect(() => string({ @@ -1328,7 +1328,7 @@ describe('_string.trimEnd', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trimEnd accepts one of the following types: string. - Received: {\\"_string.trimEnd\\":{\\"on\\":\\"abc\\"}} at locationId." + Received: {\\"_string.trimEnd\\":{\\"on\\":\\"abc\\"}} at location." `); expect(() => string({ @@ -1338,7 +1338,7 @@ describe('_string.trimEnd', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trimEnd accepts one of the following types: string. - Received: {\\"_string.trimEnd\\":null} at locationId." + Received: {\\"_string.trimEnd\\":null} at location." `); }); }); @@ -1363,7 +1363,7 @@ describe('_string.trimStart', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trimStart accepts one of the following types: string. - Received: {\\"_string.trimStart\\":[1,2]} at locationId." + Received: {\\"_string.trimStart\\":[1,2]} at location." `); expect(() => string({ @@ -1373,7 +1373,7 @@ describe('_string.trimStart', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trimStart accepts one of the following types: string. - Received: {\\"_string.trimStart\\":[\\"abc\\"]} at locationId." + Received: {\\"_string.trimStart\\":[\\"abc\\"]} at location." `); expect(() => string({ @@ -1383,7 +1383,7 @@ describe('_string.trimStart', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trimStart accepts one of the following types: string. - Received: {\\"_string.trimStart\\":{\\"on\\":\\"abc\\"}} at locationId." + Received: {\\"_string.trimStart\\":{\\"on\\":\\"abc\\"}} at location." `); expect(() => string({ @@ -1393,7 +1393,7 @@ describe('_string.trimStart', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.trimStart accepts one of the following types: string. - Received: {\\"_string.trimStart\\":null} at locationId." + Received: {\\"_string.trimStart\\":null} at location." `); }); }); @@ -1418,7 +1418,7 @@ describe('_string.length', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.length accepts one of the following types: string. - Received: {\\"_string.length\\":{\\"on\\":\\"231\\"}} at locationId." + Received: {\\"_string.length\\":{\\"on\\":\\"231\\"}} at location." `); expect(() => string({ @@ -1428,7 +1428,7 @@ describe('_string.length', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.length accepts one of the following types: string. - Received: {\\"_string.length\\":[\\"1\\"]} at locationId." + Received: {\\"_string.length\\":[\\"1\\"]} at location." `); expect(() => string({ @@ -1438,22 +1438,22 @@ describe('_string.length', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.length accepts one of the following types: string. - Received: {\\"_string.length\\":null} at locationId." + Received: {\\"_string.length\\":null} at location." `); }); }); test('_string called with no method or params', () => { - expect(() => string({ location: 'locationId' })).toThrowErrorMatchingInlineSnapshot(` + expect(() => string({ location: 'location' })).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.undefined is not supported, use one of the following: charAt, concat, endsWith, includes, indexOf, lastIndexOf, match, normalize, padEnd, padStart, repeat, replace, search, slice, split, startsWith, substring, toLowerCase, toUpperCase, trim, trimEnd, trimStart, length. - Received: {\\"_string.undefined\\":undefined} at locationId." + Received: {\\"_string.undefined\\":undefined} at location." `); }); test('_string invalid method', () => { - expect(() => string({ params: ['a'], methodName: 'X', location: 'locationId' })) + expect(() => string({ params: ['a'], methodName: 'X', location: 'location' })) .toThrowErrorMatchingInlineSnapshot(` "Operator Error: _string.X is not supported, use one of the following: charAt, concat, endsWith, includes, indexOf, lastIndexOf, match, normalize, padEnd, padStart, repeat, replace, search, slice, split, startsWith, substring, toLowerCase, toUpperCase, trim, trimEnd, trimStart, length. - Received: {\\"_string.X\\":[\\"a\\"]} at locationId." + Received: {\\"_string.X\\":[\\"a\\"]} at location." `); }); From fab9e2a3fb1d59ff604bee2b95edf0e8464f0a42 Mon Sep 17 00:00:00 2001 From: Gervwyk Date: Wed, 9 Feb 2022 10:38:25 +0200 Subject: [PATCH 15/21] fix(engine): Reset input when link is followed with no input. --- packages/engine/src/createLink.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/createLink.js b/packages/engine/src/createLink.js index 151d08fcd..285f89aa7 100644 --- a/packages/engine/src/createLink.js +++ b/packages/engine/src/createLink.js @@ -34,14 +34,14 @@ function createLink({ backLink, disabledLink, lowdefy, newOriginLink, noLink, sa ? '' : `?${urlQueryFn.stringify(props.urlQuery)}`; if (props.home === true) { - lowdefy.inputs[`page:${lowdefy.home.pageId}`] = props.input; + lowdefy.inputs[`page:${lowdefy.home.pageId}`] = props.input || {}; return sameOriginLink({ href: `/${lowdefy.home.configured ? '' : lowdefy.home.pageId}${lowdefyUrlQuery}`, ...props, }); } if (type.isString(props.pageId)) { - lowdefy.inputs[`page:${props.pageId}`] = props.input; + lowdefy.inputs[`page:${props.pageId}`] = props.input || {}; return sameOriginLink({ href: `/${props.pageId}${lowdefyUrlQuery}`, ...props }); } if (type.isString(props.url)) { From 95b44473a3f67741951e4d020a0ad84a90805d94 Mon Sep 17 00:00:00 2001 From: Gervwyk Date: Wed, 9 Feb 2022 10:39:01 +0200 Subject: [PATCH 16/21] feat(server): Add ariaLabel and rel properties to Link. --- packages/docs/concepts/custom-blocks.yaml | 2 + .../src/components/createLinkComponent.js | 58 +++++++++++++------ 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/packages/docs/concepts/custom-blocks.yaml b/packages/docs/concepts/custom-blocks.yaml index f13e1c0a4..40a4023e6 100644 --- a/packages/docs/concepts/custom-blocks.yaml +++ b/packages/docs/concepts/custom-blocks.yaml @@ -126,11 +126,13 @@ _ref: - `components: object`: Helper React components that are exposed to blocks to use internally. - `Icon`: component`: Lowdefy standard Icon React component to render build in icons. - `Link`: component`: Lowdefy standard Link React component used as links to pages or external urls. The following props apply: + - `ariaLabel: string`: Arial-label to apply to link tag. - `back: boolean`: When the link is clicked, trigger the browser back. - `home: boolean`: When the link is clicked, route to the home page. - `input: object`: When the link is clicked, pass data as the input object to the next Lowdefy page. Can only be used with pageId link and newTab false. See [Input]( TODO: Link to input page? ). - `newTab: boolean`: When the link is clicked, open the page in a new browser tab. - `pageId: string`: When the link is clicked, route to the provided Lowdefy page. + - `rel: string`: Overwrite `` tag property. - `replace: boolean`: Prevent adding a new entry into browser history by replacing the url instead of pushing into history. Can only be used with pageId link and newTab false. - `scroll: boolean`: Disable scrolling to the top of the page after page transition. Can only be used with pageId link and newTab false. - `url: string`: When the link is clicked, route to an external url. diff --git a/packages/server/src/components/createLinkComponent.js b/packages/server/src/components/createLinkComponent.js index bd4c2bf33..aa517d8fd 100644 --- a/packages/server/src/components/createLinkComponent.js +++ b/packages/server/src/components/createLinkComponent.js @@ -4,31 +4,50 @@ import { createLink } from '@lowdefy/engine'; import { type } from '@lowdefy/helpers'; const createLinkComponent = (lowdefy) => { - const backLink = ({ children, className, id }) => ( - lowdefy._internal.router.back()} className={className}> + const backLink = ({ ariaLabel, children, className, id, rel }) => ( + lowdefy._internal.router.back()} + className={className} + rel={rel} + aria-label={ariaLabel || 'back'} + > {type.isFunction(children) ? children(id) : children} ); - const newOriginLink = ({ children, className, href, id, newTab, pageId, url }) => { - return ( - - {type.isFunction(children) ? children(pageId || url || id) : children} - - ); - }; - const sameOriginLink = ({ + const newOriginLink = ({ + ariaLabel, children, className, href, id, newTab, pageId, + rel, + url, + }) => { + return ( + + {type.isFunction(children) ? children(pageId || url || id) : children} + + ); + }; + const sameOriginLink = ({ + ariaLabel, + children, + className, + href, + id, + newTab, + pageId, + rel, replace, scroll, url, @@ -37,10 +56,11 @@ const createLinkComponent = (lowdefy) => { return ( {type.isFunction(children) ? children(pageId || url || id) : children} @@ -48,7 +68,7 @@ const createLinkComponent = (lowdefy) => { } return ( - + {type.isFunction(children) ? children(pageId || url || id) : children} From 4d19d48f5bdc48442820946c18d5d08c000d9c1c Mon Sep 17 00:00:00 2001 From: JohannMoller Date: Wed, 9 Feb 2022 11:51:00 +0200 Subject: [PATCH 17/21] fix(operators-js): Fix array, function, operator, type tests --- .../src/operators/shared/array.test.js | 391 +++++++++--------- .../src/operators/shared/function.test.js | 42 +- .../src/operators/shared/operator.test.js | 61 ++- .../src/operators/shared/regex.test.js | 2 - .../src/operators/shared/type.test.js | 204 ++++----- 5 files changed, 353 insertions(+), 347 deletions(-) diff --git a/packages/plugins/operators/operators-js/src/operators/shared/array.test.js b/packages/plugins/operators/operators-js/src/operators/shared/array.test.js index 5707a8fd4..7100f47ac 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/array.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/array.test.js @@ -15,20 +15,33 @@ */ import { NodeParser } from '@lowdefy/operators'; -import array from './array.js'; -const parser = new NodeParser(); +import _args from './args.js'; +import _array from './array.js'; +import _function from './function.js'; +import _gt from './gt.js'; +import _sum from './sum.js'; + +const operators = { + _args, + _array, + _function, + _gt, + _sum, +}; + +const location = 'location'; + +const parser = new NodeParser({ operators, payload: {}, secrets: {}, user: {} }); beforeAll(async () => { await parser.init(); }); -const location = 'locationId'; - describe('_array.concat', () => { const methodName = 'concat'; test('valid', () => { expect( - array({ + _array({ params: [ [1, 2, 3], [4, 5, 6], @@ -38,14 +51,14 @@ describe('_array.concat', () => { }) ).toEqual([1, 2, 3, 4, 5, 6]); expect( - array({ + _array({ params: [null, null, null], methodName, location, }) ).toEqual([null, null]); expect( - array({ + _array({ params: [ ['b', 'c', 'a'], [1, 2, 3], @@ -58,34 +71,34 @@ describe('_array.concat', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: [1, 2], methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.concat must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.concat\\":[1,2]} at locationId." + Received: {\\"_array.concat\\":[1,2]} at location." `); expect(() => - array({ + _array({ params: { on: [] }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.concat accepts one of the following types: array. - Received: {\\"_array.concat\\":{\\"on\\":[]}} at locationId." + Received: {\\"_array.concat\\":{\\"on\\":[]}} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.concat accepts one of the following types: array. - Received: {\\"_array.concat\\":null} at locationId." + Received: {\\"_array.concat\\":null} at location." `); }); }); @@ -94,21 +107,21 @@ describe('_array.copyWithin', () => { const methodName = 'copyWithin'; test('valid', () => { expect( - array({ + _array({ params: [[1, 2, 3], 0, 1], methodName, location, }) ).toEqual([2, 3, 3]); expect( - array({ + _array({ params: { on: ['a', 'b', 'c', 'd', 'e'], target: 0, start: 2, end: 3 }, methodName, location, }) ).toEqual(['c', 'b', 'c', 'd', 'e']); expect( - array({ + _array({ params: { target: 0 }, methodName, location, @@ -117,34 +130,34 @@ describe('_array.copyWithin', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { on: 1 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.copyWithin must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.copyWithin\\":{\\"on\\":1}} at locationId." + Received: {\\"_array.copyWithin\\":{\\"on\\":1}} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.copyWithin accepts one of the following types: array, object. - Received: {\\"_array.copyWithin\\":null} at locationId." + Received: {\\"_array.copyWithin\\":null} at location." `); }); }); describe('_array.every', () => { const methodName = 'every'; - const callback = _function({ params: { __gt: [{ __args: '0' }, 3] }, parser }); + const callback = _function({ location, params: { __gt: [{ __args: '0' }, 3] }, parser }); test('valid', () => { expect( - array({ + _array({ params: { on: [4, 5, 6], callback, @@ -154,7 +167,7 @@ describe('_array.every', () => { }) ).toEqual(true); expect( - array({ + _array({ params: { on: [1, 5, 6], callback, @@ -164,7 +177,7 @@ describe('_array.every', () => { }) ).toEqual(false); expect( - array({ + _array({ params: [[4, 5, 6], callback], methodName, location, @@ -173,33 +186,33 @@ describe('_array.every', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { on: 0 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.every must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.every\\":{\\"on\\":0}} at locationId." + Received: {\\"_array.every\\":{\\"on\\":0}} at location." `); expect(() => - array({ + _array({ params: { on: [], callback: 1 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot( - `"Operator Error: _array.every - 1 is not a function Received: {\\"_array.every\\":{\\"on\\":[],\\"callback\\":1}} at locationId."` + `"Operator Error: _array.every - 1 is not a function Received: {\\"_array.every\\":{\\"on\\":[],\\"callback\\":1}} at location."` ); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.every accepts one of the following types: array, object. - Received: {\\"_array.every\\":null} at locationId." + Received: {\\"_array.every\\":null} at location." `); }); }); @@ -208,28 +221,28 @@ describe('_array.fill', () => { const methodName = 'fill'; test('valid', () => { expect( - array({ + _array({ params: [[1, 2, 3, 4, 5], 0, 1, 3], methodName, location, }) ).toEqual([1, 0, 0, 4, 5]); expect( - array({ + _array({ params: { on: ['a', 'b', 'c', 'd'], value: 'x', start: 1, end: 3 }, methodName, location, }) ).toEqual(['a', 'x', 'x', 'd']); expect( - array({ + _array({ params: [[1, 2, 3, 4], 6], methodName, location, }) ).toEqual([6, 6, 6, 6]); expect( - array({ + _array({ params: { value: 'x', start: 1, end: 2 }, methodName, location, @@ -238,34 +251,34 @@ describe('_array.fill', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: 'x', methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.fill accepts one of the following types: array, object. - Received: {\\"_array.fill\\":\\"x\\"} at locationId." + Received: {\\"_array.fill\\":\\"x\\"} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.fill accepts one of the following types: array, object. - Received: {\\"_array.fill\\":null} at locationId." + Received: {\\"_array.fill\\":null} at location." `); }); }); describe('_array.filter', () => { const methodName = 'filter'; - const callback = _function({ params: { __gt: [{ __args: '0' }, 3] }, parser }); + const callback = _function({ location, params: { __gt: [{ __args: '0' }, 3] }, parser }); test('valid', () => { expect( - array({ + _array({ params: { on: [1, 5, 6], callback, @@ -275,7 +288,7 @@ describe('_array.filter', () => { }) ).toEqual([5, 6]); expect( - array({ + _array({ params: [[1, 5, 6], callback], methodName, location, @@ -284,43 +297,43 @@ describe('_array.filter', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { on: 0 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.filter must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.filter\\":{\\"on\\":0}} at locationId." + Received: {\\"_array.filter\\":{\\"on\\":0}} at location." `); expect(() => - array({ + _array({ params: { on: [], callback: 1 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot( - `"Operator Error: _array.filter - 1 is not a function Received: {\\"_array.filter\\":{\\"on\\":[],\\"callback\\":1}} at locationId."` + `"Operator Error: _array.filter - 1 is not a function Received: {\\"_array.filter\\":{\\"on\\":[],\\"callback\\":1}} at location."` ); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.filter accepts one of the following types: array, object. - Received: {\\"_array.filter\\":null} at locationId." + Received: {\\"_array.filter\\":null} at location." `); }); }); describe('_array.find', () => { const methodName = 'find'; - const callback = _function({ params: { __gt: [{ __args: '0' }, 3] }, parser }); + const callback = _function({ location, params: { __gt: [{ __args: '0' }, 3] }, parser }); test('valid', () => { expect( - array({ + _array({ params: { on: [1, 5, 6], callback, @@ -330,7 +343,7 @@ describe('_array.find', () => { }) ).toEqual(5); expect( - array({ + _array({ params: [[1, 5, 6], callback], methodName, location, @@ -339,43 +352,43 @@ describe('_array.find', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { on: 0 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.find must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.find\\":{\\"on\\":0}} at locationId." + Received: {\\"_array.find\\":{\\"on\\":0}} at location." `); expect(() => - array({ + _array({ params: { on: [], callback: 1 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot( - `"Operator Error: _array.find - 1 is not a function Received: {\\"_array.find\\":{\\"on\\":[],\\"callback\\":1}} at locationId."` + `"Operator Error: _array.find - 1 is not a function Received: {\\"_array.find\\":{\\"on\\":[],\\"callback\\":1}} at location."` ); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.find accepts one of the following types: array, object. - Received: {\\"_array.find\\":null} at locationId." + Received: {\\"_array.find\\":null} at location." `); }); }); describe('_array.findIndex', () => { const methodName = 'findIndex'; - const callback = _function({ params: { __gt: [{ __args: '0' }, 3] }, parser }); + const callback = _function({ location, params: { __gt: [{ __args: '0' }, 3] }, parser }); test('valid', () => { expect( - array({ + _array({ params: { on: [1, 5, 6], callback, @@ -385,7 +398,7 @@ describe('_array.findIndex', () => { }) ).toEqual(1); expect( - array({ + _array({ params: [[1, 5, 6], callback], methodName, location, @@ -394,33 +407,33 @@ describe('_array.findIndex', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { on: 0 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.findIndex must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.findIndex\\":{\\"on\\":0}} at locationId." + Received: {\\"_array.findIndex\\":{\\"on\\":0}} at location." `); expect(() => - array({ + _array({ params: { on: [], callback: 1 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot( - `"Operator Error: _array.findIndex - 1 is not a function Received: {\\"_array.findIndex\\":{\\"on\\":[],\\"callback\\":1}} at locationId."` + `"Operator Error: _array.findIndex - 1 is not a function Received: {\\"_array.findIndex\\":{\\"on\\":[],\\"callback\\":1}} at location."` ); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.findIndex accepts one of the following types: array, object. - Received: {\\"_array.findIndex\\":null} at locationId." + Received: {\\"_array.findIndex\\":null} at location." `); }); }); @@ -429,21 +442,21 @@ describe('_array.flat', () => { const methodName = 'flat'; test('valid', () => { expect( - array({ + _array({ params: [[1, 2, [3], [[4]]]], methodName, location, }) ).toEqual([1, 2, 3, [4]]); expect( - array({ + _array({ params: { on: ['b', 'c', ['a'], [['c', ['v']]]], depth: 2 }, methodName, location, }) ).toEqual(['b', 'c', 'a', 'c', ['v']]); expect( - array({ + _array({ params: { depth: 1 }, methodName, location, @@ -452,24 +465,24 @@ describe('_array.flat', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: [1, 2, 3], methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.flat must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.flat\\":[1,2,3]} at locationId." + Received: {\\"_array.flat\\":[1,2,3]} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.flat accepts one of the following types: array, object. - Received: {\\"_array.flat\\":null} at locationId." + Received: {\\"_array.flat\\":null} at location." `); }); }); @@ -478,28 +491,28 @@ describe('_array.includes', () => { const methodName = 'includes'; test('valid', () => { expect( - array({ + _array({ params: [[1, 2, 3], 2], methodName, location, }) ).toEqual(true); expect( - array({ + _array({ params: { on: ['b', 'c', 'a'], value: 'c' }, methodName, location, }) ).toEqual(true); expect( - array({ + _array({ params: { on: ['b', 'c', 'a'], value: 'e' }, methodName, location, }) ).toEqual(false); expect( - array({ + _array({ params: { value: 1 }, methodName, location, @@ -508,24 +521,24 @@ describe('_array.includes', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: [1, 2, 3], methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.includes must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.includes\\":[1,2,3]} at locationId." + Received: {\\"_array.includes\\":[1,2,3]} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.includes accepts one of the following types: array, object. - Received: {\\"_array.includes\\":null} at locationId." + Received: {\\"_array.includes\\":null} at location." `); }); }); @@ -534,28 +547,28 @@ describe('_array.indexOf', () => { const methodName = 'indexOf'; test('valid', () => { expect( - array({ + _array({ params: [[1, 2, 3], 2], methodName, location, }) ).toEqual(1); expect( - array({ + _array({ params: { on: ['a', 'b', 'c'], value: 'c' }, methodName, location, }) ).toEqual(2); expect( - array({ + _array({ params: { on: ['a', 'b', 'c'], value: 'e' }, methodName, location, }) ).toEqual(-1); expect( - array({ + _array({ params: { value: 1 }, methodName, location, @@ -564,24 +577,24 @@ describe('_array.indexOf', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: [1, 2, 3], methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.indexOf must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.indexOf\\":[1,2,3]} at locationId." + Received: {\\"_array.indexOf\\":[1,2,3]} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.indexOf accepts one of the following types: array, object. - Received: {\\"_array.indexOf\\":null} at locationId." + Received: {\\"_array.indexOf\\":null} at location." `); }); }); @@ -590,21 +603,21 @@ describe('_array.join', () => { const methodName = 'join'; test('valid', () => { expect( - array({ + _array({ params: [[1, 2, 3], '-'], methodName, location, }) ).toEqual('1-2-3'); expect( - array({ + _array({ params: { on: ['a', 'b', 'c'], separator: '. ' }, methodName, location, }) ).toEqual('a. b. c'); expect( - array({ + _array({ params: { separator: '.' }, methodName, location, @@ -613,24 +626,24 @@ describe('_array.join', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: [1, 2, 3], methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.join must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.join\\":[1,2,3]} at locationId." + Received: {\\"_array.join\\":[1,2,3]} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.join accepts one of the following types: array, object. - Received: {\\"_array.join\\":null} at locationId." + Received: {\\"_array.join\\":null} at location." `); }); }); @@ -639,28 +652,28 @@ describe('_array.lastIndexOf', () => { const methodName = 'lastIndexOf'; test('valid', () => { expect( - array({ + _array({ params: [[1, 2, 3, 2], 2], methodName, location, }) ).toEqual(3); expect( - array({ + _array({ params: { on: ['c', 'a', 'c', 'b', 'c', 'x'], value: 'c' }, methodName, location, }) ).toEqual(4); expect( - array({ + _array({ params: { on: ['a', 'b', 'c'], value: 'e' }, methodName, location, }) ).toEqual(-1); expect( - array({ + _array({ params: { value: 1 }, methodName, location, @@ -669,34 +682,34 @@ describe('_array.lastIndexOf', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: [1, 2, 3], methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.lastIndexOf must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.lastIndexOf\\":[1,2,3]} at locationId." + Received: {\\"_array.lastIndexOf\\":[1,2,3]} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.lastIndexOf accepts one of the following types: array, object. - Received: {\\"_array.lastIndexOf\\":null} at locationId." + Received: {\\"_array.lastIndexOf\\":null} at location." `); }); }); describe('_array.map', () => { const methodName = 'map'; - const callback = _function({ params: { __sum: [{ __args: '0' }, 1] }, parser }); + const callback = _function({ location, params: { __sum: [{ __args: '0' }, 1] }, parser }); test('valid', () => { expect( - array({ + _array({ params: { on: [1, 5, 6], callback, @@ -706,7 +719,7 @@ describe('_array.map', () => { }) ).toEqual([2, 6, 7]); expect( - array({ + _array({ params: [[1, 5, 6], callback], methodName, location, @@ -715,43 +728,47 @@ describe('_array.map', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { on: 0 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.map must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.map\\":{\\"on\\":0}} at locationId." + Received: {\\"_array.map\\":{\\"on\\":0}} at location." `); expect(() => - array({ + _array({ params: { on: [], callback: 1 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot( - `"Operator Error: _array.map - 1 is not a function Received: {\\"_array.map\\":{\\"on\\":[],\\"callback\\":1}} at locationId."` + `"Operator Error: _array.map - 1 is not a function Received: {\\"_array.map\\":{\\"on\\":[],\\"callback\\":1}} at location."` ); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.map accepts one of the following types: array, object. - Received: {\\"_array.map\\":null} at locationId." + Received: {\\"_array.map\\":null} at location." `); }); }); describe('_array.reduce', () => { const methodName = 'reduce'; - const callback = _function({ params: { __sum: [{ __args: '0' }, { __args: '1' }] }, parser }); + const callback = _function({ + location, + params: { __sum: [{ __args: '0' }, { __args: '1' }] }, + parser, + }); test('valid', () => { expect( - array({ + _array({ params: { on: [1, 5, 6], callback, @@ -761,7 +778,7 @@ describe('_array.reduce', () => { }) ).toEqual(12); expect( - array({ + _array({ params: { on: [1, 5, 6], callback, @@ -772,7 +789,7 @@ describe('_array.reduce', () => { }) ).toEqual(20); expect( - array({ + _array({ params: [[1, 5, 6], callback], methodName, location, @@ -781,43 +798,47 @@ describe('_array.reduce', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { on: 0 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.reduce must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.reduce\\":{\\"on\\":0}} at locationId." + Received: {\\"_array.reduce\\":{\\"on\\":0}} at location." `); expect(() => - array({ + _array({ params: { on: [], callback: 1 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot( - `"Operator Error: _array.reduce - 1 is not a function Received: {\\"_array.reduce\\":{\\"on\\":[],\\"callback\\":1}} at locationId."` + `"Operator Error: _array.reduce - 1 is not a function Received: {\\"_array.reduce\\":{\\"on\\":[],\\"callback\\":1}} at location."` ); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.reduce accepts one of the following types: array, object. - Received: {\\"_array.reduce\\":null} at locationId." + Received: {\\"_array.reduce\\":null} at location." `); }); }); describe('_array.reduceRight', () => { const methodName = 'reduceRight'; - const callback = _function({ params: { __sum: [{ __args: '0' }, { __args: '1' }] }, parser }); + const callback = _function({ + location, + params: { __sum: [{ __args: '0' }, { __args: '1' }] }, + parser, + }); test('valid', () => { expect( - array({ + _array({ params: { on: [1, 5, 6], callback, @@ -827,7 +848,7 @@ describe('_array.reduceRight', () => { }) ).toEqual(12); expect( - array({ + _array({ params: { on: [1, 5, 6], callback, @@ -838,7 +859,7 @@ describe('_array.reduceRight', () => { }) ).toEqual(20); expect( - array({ + _array({ params: [[1, 5, 6], callback], methodName, location, @@ -847,33 +868,33 @@ describe('_array.reduceRight', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { on: 0 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.reduceRight must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.reduceRight\\":{\\"on\\":0}} at locationId." + Received: {\\"_array.reduceRight\\":{\\"on\\":0}} at location." `); expect(() => - array({ + _array({ params: { on: [], callback: 1 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot( - `"Operator Error: _array.reduceRight - 1 is not a function Received: {\\"_array.reduceRight\\":{\\"on\\":[],\\"callback\\":1}} at locationId."` + `"Operator Error: _array.reduceRight - 1 is not a function Received: {\\"_array.reduceRight\\":{\\"on\\":[],\\"callback\\":1}} at location."` ); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.reduceRight accepts one of the following types: array, object. - Received: {\\"_array.reduceRight\\":null} at locationId." + Received: {\\"_array.reduceRight\\":null} at location." `); }); }); @@ -882,7 +903,7 @@ describe('_array.reverse', () => { const methodName = 'reverse'; test('valid', () => { expect( - array({ + _array({ params: [1, 2, 3], methodName, location, @@ -891,34 +912,34 @@ describe('_array.reverse', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { on: [1, 2, 3] }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.reverse accepts one of the following types: array. - Received: {\\"_array.reverse\\":{\\"on\\":[1,2,3]}} at locationId." + Received: {\\"_array.reverse\\":{\\"on\\":[1,2,3]}} at location." `); expect(() => - array({ + _array({ params: '[1, 2, 3]', methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.reverse accepts one of the following types: array. - Received: {\\"_array.reverse\\":\\"[1, 2, 3]\\"} at locationId." + Received: {\\"_array.reverse\\":\\"[1, 2, 3]\\"} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.reverse accepts one of the following types: array. - Received: {\\"_array.reverse\\":null} at locationId." + Received: {\\"_array.reverse\\":null} at location." `); }); }); @@ -927,21 +948,21 @@ describe('_array.slice', () => { const methodName = 'slice'; test('valid', () => { expect( - array({ + _array({ params: [[1, 2, 3, 4, 5], 1, 3], methodName, location, }) ).toEqual([2, 3]); expect( - array({ + _array({ params: { on: ['b', 'c', 'a'], start: 1 }, methodName, location, }) ).toEqual(['c', 'a']); expect( - array({ + _array({ params: { start: 1 }, methodName, location, @@ -950,24 +971,24 @@ describe('_array.slice', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: 1, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.slice accepts one of the following types: array, object. - Received: {\\"_array.slice\\":1} at locationId." + Received: {\\"_array.slice\\":1} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.slice accepts one of the following types: array, object. - Received: {\\"_array.slice\\":null} at locationId." + Received: {\\"_array.slice\\":null} at location." `); }); }); @@ -976,14 +997,14 @@ describe('_array.splice', () => { const methodName = 'splice'; test('valid', () => { expect( - array({ + _array({ params: [['b', 'c', 'a'], 1, 0, 1, 2, 3], methodName, location, }) ).toEqual(['b', 1, 2, 3, 'c', 'a']); expect( - array({ + _array({ params: { on: ['b', 'c', 'a'], start: 1, end: 0, insert: [1, 2, 3] }, methodName, location, @@ -992,44 +1013,44 @@ describe('_array.splice', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { start: 1 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.splice takes an array as input argument for insert. - Received: {\\"_array.splice\\":{\\"start\\":1}} at locationId." + Received: {\\"_array.splice\\":{\\"start\\":1}} at location." `); expect(() => - array({ + _array({ params: 1, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.splice accepts one of the following types: array, object. - Received: {\\"_array.splice\\":1} at locationId." + Received: {\\"_array.splice\\":1} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.splice accepts one of the following types: array, object. - Received: {\\"_array.splice\\":null} at locationId." + Received: {\\"_array.splice\\":null} at location." `); }); }); describe('_array.some', () => { const methodName = 'some'; - const callback = _function({ params: { __gt: [{ __args: '0' }, 3] }, parser }); + const callback = _function({ location, params: { __gt: [{ __args: '0' }, 3] }, parser }); test('valid', () => { expect( - array({ + _array({ params: { on: [1, 2, 2], callback, @@ -1039,7 +1060,7 @@ describe('_array.some', () => { }) ).toEqual(false); expect( - array({ + _array({ params: { on: [1, 5, 6], callback, @@ -1049,7 +1070,7 @@ describe('_array.some', () => { }) ).toEqual(true); expect( - array({ + _array({ params: [[1, 5, 6], callback], methodName, location, @@ -1058,33 +1079,33 @@ describe('_array.some', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { on: 0 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.some must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.some\\":{\\"on\\":0}} at locationId." + Received: {\\"_array.some\\":{\\"on\\":0}} at location." `); expect(() => - array({ + _array({ params: { on: [], callback: 1 }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot( - `"Operator Error: _array.some - 1 is not a function Received: {\\"_array.some\\":{\\"on\\":[],\\"callback\\":1}} at locationId."` + `"Operator Error: _array.some - 1 is not a function Received: {\\"_array.some\\":{\\"on\\":[],\\"callback\\":1}} at location."` ); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.some accepts one of the following types: array, object. - Received: {\\"_array.some\\":null} at locationId." + Received: {\\"_array.some\\":null} at location." `); }); }); @@ -1093,14 +1114,14 @@ describe('_array.sort', () => { const methodName = 'sort'; test('valid', () => { expect( - array({ + _array({ params: [[4, 1, 2, 3]], methodName, location, }) ).toEqual([1, 2, 3, 4]); expect( - array({ + _array({ params: [['b', 'e', 'c', 'a']], methodName, location, @@ -1109,34 +1130,34 @@ describe('_array.sort', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: [1, 2], methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.sort must be evaluated on an array instance. For named args provide an array instance to the \\"on\\" property, for listed args provide and array instance as the first element in the operator argument array. - Received: {\\"_array.sort\\":[1,2]} at locationId." + Received: {\\"_array.sort\\":[1,2]} at location." `); expect(() => - array({ + _array({ params: { on: [] }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.sort accepts one of the following types: array. - Received: {\\"_array.sort\\":{\\"on\\":[]}} at locationId." + Received: {\\"_array.sort\\":{\\"on\\":[]}} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.sort accepts one of the following types: array. - Received: {\\"_array.sort\\":null} at locationId." + Received: {\\"_array.sort\\":null} at location." `); }); }); @@ -1145,21 +1166,21 @@ describe('_array.length', () => { const methodName = 'length'; test('valid', () => { expect( - array({ + _array({ params: [[1, 2, 3], 2], methodName, location, }) ).toEqual(2); expect( - array({ + _array({ params: [1, 2, 3, 4], methodName, location, }) ).toEqual(4); expect( - array({ + _array({ params: [], methodName, location, @@ -1168,49 +1189,49 @@ describe('_array.length', () => { }); test('throw', () => { expect(() => - array({ + _array({ params: { on: [1] }, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.length accepts one of the following types: array. - Received: {\\"_array.length\\":{\\"on\\":[1]}} at locationId." + Received: {\\"_array.length\\":{\\"on\\":[1]}} at location." `); expect(() => - array({ + _array({ params: '1', methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.length accepts one of the following types: array. - Received: {\\"_array.length\\":\\"1\\"} at locationId." + Received: {\\"_array.length\\":\\"1\\"} at location." `); expect(() => - array({ + _array({ params: null, methodName, location, }) ).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.length accepts one of the following types: array. - Received: {\\"_array.length\\":null} at locationId." + Received: {\\"_array.length\\":null} at location." `); }); }); test('_array called with no method or params', () => { - expect(() => array({ location: 'locationId' })).toThrowErrorMatchingInlineSnapshot(` + expect(() => _array({ location: 'location' })).toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.undefined is not supported, use one of the following: concat, copyWithin, every, fill, filter, find, findIndex, flat, includes, indexOf, join, lastIndexOf, map, reduce, reduceRight, reverse, slice, some, sort, splice, length. - Received: {\\"_array.undefined\\":undefined} at locationId." + Received: {\\"_array.undefined\\":undefined} at location." `); }); test('_array invalid method', () => { - expect(() => array({ params: [['a']], methodName: 'X', location: 'locationId' })) + expect(() => _array({ params: [['a']], methodName: 'X', location: 'location' })) .toThrowErrorMatchingInlineSnapshot(` "Operator Error: _array.X is not supported, use one of the following: concat, copyWithin, every, fill, filter, find, findIndex, flat, includes, indexOf, join, lastIndexOf, map, reduce, reduceRight, reverse, slice, some, sort, splice, length. - Received: {\\"_array.X\\":[[\\"a\\"]]} at locationId." + Received: {\\"_array.X\\":[[\\"a\\"]]} at location." `); }); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/function.test.js b/packages/plugins/operators/operators-js/src/operators/shared/function.test.js index 23df548ad..d2e3cf85b 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/function.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/function.test.js @@ -15,12 +15,19 @@ */ import { NodeParser, WebParser } from '@lowdefy/operators'; import _function from './function.js'; +import _args from './args.js'; +import _payload from '../server/payload.js'; +import _state from '../client/state.js'; const operators = { - _test: jest.fn(() => 'test'), - _error: jest.fn(() => { - throw new Error('Test error.'); - }), + _args, + _function, + _payload, + _state, + // _test: jest.fn(() => 'test'), + // _error: jest.fn(() => { + // throw new Error('Test error.'); + // }), }; const state = { @@ -37,25 +44,34 @@ const payload = { const location = 'location'; const context = { - lowdefy: { inputs: {} }, + _internal: { + lowdefy: { + inputs: { id: true }, + lowdefyGlobal: { global: true }, + menus: [{ menus: true }], + urlQuery: { urlQuery: true }, + user: { user: true }, + }, + }, + eventLog: [{ eventLog: true }], + id: 'id', + requests: [{ requests: true }], state, - operators, }; console.error = () => {}; test('NodeParser, _function that gets from payload', async () => { - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload, secrets: {}, user: {} }); await parser.init(); const params = { __payload: 'string' }; const fn = _function({ location, params, parser }); expect(fn).toBeInstanceOf(Function); expect(fn()).toEqual('Some String'); - expect(fn()).toEqual('Some String'); }); test('NodeParser, _function gives args as an array', async () => { - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload, secrets: {}, user: {} }); await parser.init(); const params = { __args: true }; const fn = _function({ location, params, parser }); @@ -64,7 +80,7 @@ test('NodeParser, _function gives args as an array', async () => { }); test('NodeParser, _function throws on parser errors', async () => { - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload, secrets: {}, user: {} }); await parser.init(); const params = { __payload: [] }; const fn = _function({ location, params, parser }); @@ -74,7 +90,7 @@ test('NodeParser, _function throws on parser errors', async () => { }); test('WebParser, _function that gets from state', async () => { - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const params = { __state: 'string' }; const fn = _function({ location, params, parser }); @@ -84,7 +100,7 @@ test('WebParser, _function that gets from state', async () => { }); test('WebParser, _function gives args as an array', async () => { - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const params = { __args: true }; const fn = _function({ location, params, parser }); @@ -93,7 +109,7 @@ test('WebParser, _function gives args as an array', async () => { }); test('WebParser, _function throws on parser errors', async () => { - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const params = { __state: [] }; const fn = _function({ location, params, parser }); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/operator.test.js b/packages/plugins/operators/operators-js/src/operators/shared/operator.test.js index 2498ce39f..c66e8f10e 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/operator.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/operator.test.js @@ -15,6 +15,25 @@ */ import { NodeParser } from '@lowdefy/operators'; +import _args from './args.js'; +import _function from './function.js'; +import _json from './json.js'; +import _not from './not.js'; +import _payload from '../server/payload.js'; +import _operator from './operator.js'; +import _state from '../client/state.js'; + +const operators = { + _args, + _function, + _json, + _not, + _payload, + _operator, + _state, +}; + +const location = 'location'; const payload = { string: 'Some String', @@ -26,9 +45,9 @@ console.error = () => {}; test('_operator, _payload', async () => { const input = { a: { _operator: { name: '_payload', params: 'string' } } }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload }); await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); + const res = parser.parse({ input, location }); expect(res.output).toEqual({ a: 'Some String', }); @@ -37,83 +56,83 @@ test('_operator, _payload', async () => { test('_operator.name invalid', async () => { const input = { a: { _operator: { name: '_a' } } }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload }); await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); + const res = parser.parse({ input, location }); expect(res.output).toEqual({ a: null }); expect(res.errors).toMatchInlineSnapshot(` Array [ - [Error: Operator Error: _operator - Invalid operator name. Received: {"name":"_a"} at locationId.], + [Error: Operator Error: _operator - Invalid operator name. Received: {"name":"_a"} at location.], ] `); }); test('_operator.name not allowed to include "experimental"', async () => { const input = { a: { _operator: { name: '_experimental_op' } } }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload }); await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); + const res = parser.parse({ input, location }); expect(res.output).toEqual({ a: null }); expect(res.errors).toMatchInlineSnapshot(` Array [ - [Error: Operator Error: Experimental operators cannot be used with _operator. Received: {"name":"_experimental_op"} at locationId.], + [Error: Operator Error: Experimental operators cannot be used with _operator. Received: {"name":"_experimental_op"} at location.], ] `); }); test('_operator.name not a string', async () => { const input = { a: { _operator: { name: 1 } } }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload }); await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); + const res = parser.parse({ input, location }); expect(res.output).toEqual({ a: null }); expect(res.errors).toMatchInlineSnapshot(` Array [ - [Error: Operator Error: _operator.name must be a valid operator name as string. Received: {"name":1} at locationId.], + [Error: Operator Error: _operator.name must be a valid operator name as string. Received: {"name":1} at location.], ] `); }); test('_operator with value not a object', async () => { const input = { a: { _operator: 'a' } }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload }); await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); + const res = parser.parse({ input, location }); expect(res.output).toEqual({ a: null }); expect(res.errors).toMatchInlineSnapshot(` Array [ - [Error: Operator Error: _operator.name must be a valid operator name as string. Received: "a" at locationId.], + [Error: Operator Error: _operator.name must be a valid operator name as string. Received: "a" at location.], ] `); }); test('_operator cannot be set to _operator', async () => { const input = { a: { _operator: { name: '_operator' } } }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload }); await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); + const res = parser.parse({ input, location }); expect(res.output).toEqual({ a: null }); expect(res.errors).toMatchInlineSnapshot(` Array [ - [Error: Operator Error: _operator.name cannot be set to _operator to infinite avoid loop reference. Received: {"name":"_operator"} at locationId.], + [Error: Operator Error: _operator.name cannot be set to _operator to infinite avoid loop reference. Received: {"name":"_operator"} at location.], ] `); }); test('_operator, _not with no params', async () => { const input = { a: { _operator: { name: '_not' } } }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload }); await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); + const res = parser.parse({ input, location }); expect(res.output).toEqual({ a: true }); expect(res.errors).toMatchInlineSnapshot(`Array []`); }); test('_operator, _json.parse with params', async () => { const input = { a: { _operator: { name: '_json.parse', params: '[{ "a": "a1"}]' } } }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload }); await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); + const res = parser.parse({ input, location }); expect(res.output).toEqual({ a: [{ a: 'a1' }], }); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/regex.test.js b/packages/plugins/operators/operators-js/src/operators/shared/regex.test.js index e494039c8..6d1320850 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/regex.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/regex.test.js @@ -13,8 +13,6 @@ See the License for the specific language governing permissions and limitations under the License. */ - -import { NodeParser } from '@lowdefy/operators'; import _regex from './regex.js'; const location = 'location'; diff --git a/packages/plugins/operators/operators-js/src/operators/shared/type.test.js b/packages/plugins/operators/operators-js/src/operators/shared/type.test.js index 1a8e1c5db..3eb3d4bcd 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/type.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/type.test.js @@ -15,6 +15,15 @@ */ import { NodeParser } from '@lowdefy/operators'; +import _type from './type.js'; +import _date from './date.js'; + +const operators = { + _date, + _type, +}; + +const location = 'location'; const state = { string: 'Some String', @@ -25,136 +34,79 @@ const state = { console.error = () => {}; -test('_type with on, pass', async () => { - const input = { _type: { type: 'string', on: 'a' } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); +test('_type with on, pass', () => { + expect(_type({ params: { type: 'string', on: 'a' }, location })).toEqual(true); +}); +test('_type with on, fail', () => { + expect(_type({ params: { type: 'number', on: 'a' }, location })).toEqual(false); +}); +test('_type with key, pass', () => { + expect(_type({ params: { type: 'string', key: 'string' }, location, state })).toEqual(true); +}); +test('_type with key, fail', () => { + expect(_type({ params: { type: 'string', key: 'number' }, location, state })).toEqual(false); +}); +test('_type with null on, pass', () => { + expect(_type({ params: { type: 'null', on: null }, location })).toEqual(true); +}); +test('_type with null on, fail', () => { + expect(_type({ params: { type: 'boolean', on: null }, location })).toEqual(false); +}); +test('_type with nonexistent key', () => { + expect(_type({ params: { type: 'boolean', key: 'notThere' }, location, state })).toEqual(false); +}); +test('_type with null key', () => { + expect(_type({ params: { type: 'boolean', key: null }, location, state })).toEqual(false); +}); +test('_type null', () => { + expect(() => _type({ params: null, location })).toThrow( + 'Operator Error: _type.type must be a string. Received: null at location.' + ); +}); +test('_type with non-string on', () => { + expect(_type({ params: { type: 'number', on: 5 }, location })).toEqual(true); +}); +test('_type with unknown type', () => { + expect(() => _type({ params: { type: 'strings' }, location })).toThrow( + 'Operator Error: "strings" is not a valid _type test. Received: {"type":"strings"} at location.' + ); +}); +test('_type date on string date fail', () => { + expect(_type({ params: { type: 'date', on: '2019-11-28T08:10:09.844Z' }, location })).toEqual( + false + ); +}); +test('_type date on date object pass', () => { + expect(_type({ params: { type: 'date', on: new Date() }, location })).toEqual(true); }); -test('_type with on, fail', async () => { - const input = { _type: { type: 'number', on: 'b' } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); +test('_type array', () => { + expect(_type({ params: { type: 'array', key: 'arr' }, location, state })).toEqual(true); +}); +test('_type object', () => { + expect(_type({ params: { type: 'object', on: { key: 'value' } }, location, state })).toEqual( + true + ); +}); +test('_type primitive', () => { + expect(_type({ params: { type: 'primitive', on: 'Primitive string' }, location, state })).toEqual( + true + ); +}); +test('_type integer', () => { + expect(_type({ params: { type: 'integer', on: 42 }, location, state })).toEqual(true); +}); +test('_type undefined', () => { + expect(_type({ params: { type: 'undefined', on: undefined }, location, state })).toEqual(true); +}); +test('_type none', () => { + expect(_type({ params: { type: 'none' }, location, state })).toEqual(true); }); -// NOTE: key not supported by NodeParser -// test('_type with key, pass', async () => { -// const input = { _type: { type: 'string', key: 'string' } }; -// const parser = new NodeParser({ state }); -// await parser.init(); -// const res = parser.parse({ input, location: 'locationId' }); -// expect(res.output).toBe(true); -// expect(res.errors).toMatchInlineSnapshot(`Array []`); -// }); - -// test('_type with key, fail', async () => { -// const input = { _type: { type: 'number', key: 'string' } }; -// const parser = new NodeParser({ state }); -// await parser.init(); -// const res = parser.parse({ input, location: 'locationId' }); -// expect(res.output).toBe(false); -// expect(res.errors).toMatchInlineSnapshot(`Array []`); -// }); - -test('_type with null on pass', async () => { - const input = { _type: { type: 'null', on: null } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); -test('_type with null on fail', async () => { - const input = { _type: { type: 'boolean', on: null } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_type with nonexistent key', async () => { - const input = { _type: { type: 'string', key: 'notThere' } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_type with nonexistent key', async () => { - const input = { _type: { type: 'string', key: null } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_type null', async () => { - const input = { _type: null }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: _type.type must be a string. Received: null at locationId.], - ] - `); -}); - -test('_type with non-string on', async () => { - const input = { _type: { type: 'number', on: 5 } }; - const parser = new NodeParser({ state }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_type with unknown type', async () => { - const input = { _type: 'strings' }; - const parser = new NodeParser({ state, arrayIndices: [] }); - await parser.init(); - const res = parser.parse({ input, location: 'locationId' }); - expect(res.output).toBe(null); - expect(res.errors).toMatchInlineSnapshot(` - Array [ - [Error: Operator Error: "strings" is not a valid _type test. Received: "strings" at locationId.], - ] - `); -}); - -test('_type date with on packed date pass', async () => { +test('_type date with on packed date pass and calls NodeParser', async () => { const input = { _type: { type: 'date', on: { _date: Date.now() } } }; - const parser = new NodeParser({ state, arrayIndices: [] }); + const parser = new NodeParser({ operators, payload: {}, secrets: {}, user: {} }); await parser.init(); - const res = parser.parse({ input, id: '1', location: 'locationId' }); - expect(res.output).toBe(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_type date on string date fail', async () => { - const input = { _type: { type: 'date', on: '2019-11-28T08:10:09.844Z' } }; - const parser = new NodeParser({ state, arrayIndices: [] }); - await parser.init(); - const res = parser.parse({ input, id: '1', location: 'locationId' }); - expect(res.output).toBe(false); - expect(res.errors).toMatchInlineSnapshot(`Array []`); -}); - -test('_type date on date object pass', async () => { - const input = { _type: { type: 'date', on: new Date() } }; - const parser = new NodeParser({ state, arrayIndices: [] }); - await parser.init(); - const res = parser.parse({ input, id: '1', location: 'locationId' }); - expect(res.output).toBe(true); - expect(res.errors).toMatchInlineSnapshot(`Array []`); + const res = parser.parse({ input, location }); + expect(res.output).toEqual(true); }); From 3fac862c58dea6b293d9556f4e8f17dd169021d8 Mon Sep 17 00:00:00 2001 From: JohannMoller Date: Wed, 9 Feb 2022 12:39:04 +0200 Subject: [PATCH 18/21] fix(operators-js): Fix menu and location tests --- .../src/operators/client/location.test.js | 2 +- .../src/operators/client/menu.test.js | 41 +++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/plugins/operators/operators-js/src/operators/client/location.test.js b/packages/plugins/operators/operators-js/src/operators/client/location.test.js index 2d903c275..36efee5d7 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/location.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/location.test.js @@ -47,7 +47,7 @@ const input = { }; test('location calls getFromObject', async () => { - const lowdefyOperators = import('@lowdefy/operators'); + const lowdefyOperators = await import('@lowdefy/operators'); location(input); expect(lowdefyOperators.getFromObject.mock.calls).toEqual([ [ diff --git a/packages/plugins/operators/operators-js/src/operators/client/menu.test.js b/packages/plugins/operators/operators-js/src/operators/client/menu.test.js index 906199979..3b68e39c8 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/menu.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/menu.test.js @@ -16,6 +16,7 @@ /* eslint-disable max-classes-per-file */ import { WebParser } from '@lowdefy/operators'; +import _menu from './menu.js'; const arrayIndices = [1]; @@ -24,7 +25,17 @@ const context = { lowdefy: { inputs: { id: true }, lowdefyGlobal: { global: true }, - menus: [{ menus: true }], + menus: [ + { + menuId: 'default', + }, + { + menuId: 'm_1', + }, + { + menuId: 'm_2', + }, + ], urlQuery: { urlQuery: true }, user: { user: true }, }, @@ -35,11 +46,15 @@ const context = { state: { state: true }, }; +const operators = { + _menu, +}; + console.error = () => {}; test('_menu using string menuId', async () => { const input = { a: { _menu: 'default' } }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual({ @@ -52,7 +67,7 @@ test('_menu using string menuId', async () => { test('_menu using index', async () => { const input = { a: { _menu: 1 } }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual({ @@ -65,7 +80,7 @@ test('_menu using index', async () => { test('_menu in object', async () => { const input = { a: { _menu: 'default' } }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual({ @@ -78,7 +93,7 @@ test('_menu in object', async () => { test('_menu full menus', async () => { const input = { _menu: true }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual([ @@ -97,7 +112,7 @@ test('_menu full menus', async () => { test('_menu null', async () => { const input = { _menu: null }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toBe(null); @@ -114,7 +129,7 @@ test('_menu param object value', async () => { value: 'm_2', }, }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual({ menuId: 'm_2' }); @@ -127,7 +142,7 @@ test('_menu param object index', async () => { index: 2, }, }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual({ menuId: 'm_2' }); @@ -140,7 +155,7 @@ test('_menu params object value not string', async () => { value: 1, }, }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toBe(null); @@ -157,7 +172,7 @@ test('_menu params object index not number', async () => { index: 'a', }, }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toBe(null); @@ -174,7 +189,7 @@ test('_menu param object all', async () => { all: true, }, }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual([ @@ -198,7 +213,7 @@ test('_menu param object all and value', async () => { value: 'default', }, }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual([ @@ -221,7 +236,7 @@ test('_menu param object invalid', async () => { other: true, }, }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual(null); From f78405f48602f97b6954ceb07bf2e9dfb5bf5e7a Mon Sep 17 00:00:00 2001 From: JohannMoller Date: Wed, 9 Feb 2022 13:58:45 +0200 Subject: [PATCH 19/21] fix(operators-js): Update request tests. --- .../src/operators/client/request.test.js | 41 ++++++++++++++----- .../src/operators/shared/function.test.js | 4 -- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/plugins/operators/operators-js/src/operators/client/request.test.js b/packages/plugins/operators/operators-js/src/operators/client/request.test.js index 1708e95a7..e94cc1b53 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/request.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/request.test.js @@ -15,6 +15,11 @@ */ import { WebParser } from '@lowdefy/operators'; +import _request from './request.js'; + +const operators = { + _request, +}; const arrayIndices = [1]; @@ -30,7 +35,23 @@ const context = { }, eventLog: [{ eventLog: true }], id: 'id', - requests: [{ requests: true }], + requests: { + arr: { + response: [{ a: 'request a1' }, { a: 'request a2' }], + loading: false, + error: [], + }, + number: { + response: 500, + loading: false, + error: [], + }, + string: { + response: 'request String', + loading: false, + error: [], + }, + }, state: { state: true }, }; @@ -38,7 +59,7 @@ console.error = () => {}; test('_request by id', async () => { const input = { a: { _request: 'string' } }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual({ @@ -49,7 +70,7 @@ test('_request by id', async () => { test('_request true gives null', async () => { const input = { _request: true }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual(null); @@ -62,7 +83,7 @@ test('_request true gives null', async () => { test('_request return full array', async () => { const input = { _request: 'arr' }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual([{ a: 'request a1' }, { a: 'request a2' }]); @@ -71,7 +92,7 @@ test('_request return full array', async () => { test('_request return number', async () => { const input = { _request: 'number' }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toBe(500); @@ -80,7 +101,7 @@ test('_request return number', async () => { test('_request null', async () => { const input = { _request: null }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toBe(null); @@ -93,7 +114,7 @@ test('_request null', async () => { test('_request loading true', async () => { const input = { _request: 'not_loaded' }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toBe(null); @@ -102,7 +123,7 @@ test('_request loading true', async () => { test('_request dot notation', async () => { const input = { _request: 'arr.0.a' }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual('request a1'); @@ -111,7 +132,7 @@ test('_request dot notation', async () => { test('_request dot notation with arrayindices', async () => { const input = { _request: 'arr.$.a' }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual('request a2'); @@ -120,7 +141,7 @@ test('_request dot notation with arrayindices', async () => { test('_request dot notation returns null if ', async () => { const input = { _request: 'returnsNull.key' }; - const parser = new WebParser({ context }); + const parser = new WebParser({ context, operators }); await parser.init(); const res = parser.parse({ input, location: 'locationId', arrayIndices }); expect(res.output).toEqual(null); diff --git a/packages/plugins/operators/operators-js/src/operators/shared/function.test.js b/packages/plugins/operators/operators-js/src/operators/shared/function.test.js index d2e3cf85b..8957ffe8c 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/function.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/function.test.js @@ -24,10 +24,6 @@ const operators = { _function, _payload, _state, - // _test: jest.fn(() => 'test'), - // _error: jest.fn(() => { - // throw new Error('Test error.'); - // }), }; const state = { From 6b5d8c81b1a14a98c0dfc0f4febb87f0fb6f6ba0 Mon Sep 17 00:00:00 2001 From: JohannMoller Date: Wed, 9 Feb 2022 14:03:12 +0200 Subject: [PATCH 20/21] fix(operators-nunjucks): Fix tests. --- .../src/operators/shared/nunjucks.test.js | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/packages/plugins/operators/operators-nunjucks/src/operators/shared/nunjucks.test.js b/packages/plugins/operators/operators-nunjucks/src/operators/shared/nunjucks.test.js index d289acb85..71a100222 100644 --- a/packages/plugins/operators/operators-nunjucks/src/operators/shared/nunjucks.test.js +++ b/packages/plugins/operators/operators-nunjucks/src/operators/shared/nunjucks.test.js @@ -14,24 +14,11 @@ limitations under the License. */ -import { NodeParser, WebParser } from '@lowdefy/operators'; +import { NodeParser } from '@lowdefy/operators'; +import _nunjucks from './nunjucks.js'; -const arrayIndices = [1]; - -const context = { - _internal: { - lowdefy: { - inputs: { id: true }, - lowdefyGlobal: { global: true }, - menus: [{ menus: true }], - urlQuery: { urlQuery: true }, - user: { user: true }, - }, - }, - eventLog: [{ eventLog: true }], - id: 'id', - requests: [{ requests: true }], - state: { state: true }, +const operators = { + _nunjucks, }; const payload = { @@ -44,7 +31,7 @@ console.error = () => {}; test('_nunjucks string template', async () => { const input = { _nunjucks: 'String with {{ string }} embedded' }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload, secrets: {}, user: {} }); await parser.init(); const res = parser.parse({ input, location: 'locationId' }); expect(res.output).toEqual('String with Some String embedded'); @@ -53,7 +40,7 @@ test('_nunjucks string template', async () => { test('_nunjucks null', async () => { const input = { _nunjucks: null }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload, secrets: {}, user: {} }); await parser.init(); const res = parser.parse({ input, location: 'locationId' }); expect(res.output).toBe(null); @@ -64,7 +51,7 @@ test('_nunjucks { template: , on: }', async () => { const input = { _nunjucks: { template: 'String with {{ string }} embedded', on: { string: 'test' } }, }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload, secrets: {}, user: {} }); await parser.init(); const res = parser.parse({ input, location: 'locationId' }); expect(res.output).toEqual('String with test embedded'); @@ -73,7 +60,7 @@ test('_nunjucks { template: , on: }', async () => { test('_nunjucks template not a string', async () => { const input = { _nunjucks: ['String with {{ string }} embedded'] }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload, secrets: {}, user: {} }); await parser.init(); const res = parser.parse({ input, location: 'locationId' }); expect(res.output).toBe(null); @@ -84,7 +71,7 @@ test('_nunjucks params on template not a string', async () => { const input = { _nunjucks: { template: ['String with {{ string }} embedded'], on: { string: 'test' } }, }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload, secrets: {}, user: {} }); await parser.init(); const res = parser.parse({ input, location: 'locationId' }); expect(res.output).toBe(null); @@ -95,7 +82,7 @@ test('_nunjucks on not a object', async () => { const input = { _nunjucks: { template: 'String with {{ string }} embedded', on: [{ string: 'test' }] }, }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload, secrets: {}, user: {} }); await parser.init(); const res = parser.parse({ input, location: 'locationId' }); expect(res.output).toBe('String with embedded'); @@ -106,7 +93,7 @@ test('_nunjucks on null', async () => { const input = { _nunjucks: { template: 'String with {{ string }} embedded', on: null }, }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload, secrets: {}, user: {} }); await parser.init(); const res = parser.parse({ input, location: 'locationId' }); expect(res.output).toBe('String with embedded'); @@ -115,7 +102,7 @@ test('_nunjucks on null', async () => { test('_nunjucks invalid template', async () => { const input = { _nunjucks: 'String with {{ string embedded' }; - const parser = new NodeParser({ payload }); + const parser = new NodeParser({ operators, payload, secrets: {}, user: {} }); await parser.init(); const res = parser.parse({ input, location: 'locationId' }); expect(res.output).toBe(null); From 708beacc11f8de1c09c7abef3fae724f74c70d6a Mon Sep 17 00:00:00 2001 From: JohannMoller Date: Wed, 9 Feb 2022 16:00:39 +0200 Subject: [PATCH 21/21] fix(operators-js): Update test name and license wording. --- .../operators/operators-js/src/operators/shared/and.test.js | 4 ++-- .../operators/operators-js/src/operators/shared/or.test.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/plugins/operators/operators-js/src/operators/shared/and.test.js b/packages/plugins/operators/operators-js/src/operators/shared/and.test.js index 1affc7945..e77790808 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/and.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/and.test.js @@ -49,7 +49,7 @@ test('_and errors', () => { ); }); -test('_and calls NodeParser', async () => { +test('_and evaluated in NodeParser', async () => { const input = { a: { _and: [true, true] } }; const parser = new NodeParser({ operators, payload: {}, secrets: {}, user: {} }); await parser.init(); @@ -57,7 +57,7 @@ test('_and calls NodeParser', async () => { expect(res.output).toEqual({ a: true }); }); -test('_and calls WebParser', async () => { +test('_and evaluated in WebParser', async () => { const context = { _internal: { lowdefy: { diff --git a/packages/plugins/operators/operators-js/src/operators/shared/or.test.js b/packages/plugins/operators/operators-js/src/operators/shared/or.test.js index 3f6b8feea..e6c1416b0 100644 --- a/packages/plugins/operators/operators-js/src/operators/shared/or.test.js +++ b/packages/plugins/operators/operators-js/src/operators/shared/or.test.js @@ -10,7 +10,7 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions or + See the License for the specific language governing permissions and limitations under the License. */ import { NodeParser, WebParser } from '@lowdefy/operators'; @@ -49,7 +49,7 @@ test('_or errors', () => { ); }); -test('_or calls NodeParser', async () => { +test('_or evaluated in NodeParser', async () => { const input = { a: { _or: [true, false] } }; const parser = new NodeParser({ operators, payload: {}, secrets: {}, user: {} }); await parser.init(); @@ -57,7 +57,7 @@ test('_or calls NodeParser', async () => { expect(res.output).toEqual({ a: true }); }); -test('_or calls WebParser', async () => { +test('_or evaluated in WebParser', async () => { const context = { _internal: { lowdefy: {