Merge pull request #762 from lowdefy/debounce-events

Debounce events
This commit is contained in:
Gerrie van Wyk 2021-08-19 16:25:24 +02:00 committed by GitHub
commit 4bde58f654
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 566 additions and 47 deletions

View File

@ -45,11 +45,11 @@ const ConfirmModal = ({ blockId, events, content, methods, properties }) => {
zIndex: properties.zIndex,
onOk: async () => {
const response = await methods.triggerEvent({ name: 'onOk' });
if (response.success === false) throw response;
if (response.success === false && response.bounced !== true) throw response;
},
onCancel: async () => {
const response = await methods.triggerEvent({ name: 'onCancel' });
if (response.success === false) throw response;
if (response.success === false && response.bounced !== true) throw response;
},
...additionalProps,
});

View File

@ -59,7 +59,9 @@ const DrawerBlock = ({ blockId, content, properties, methods, rename, onClose })
(async () => {
const response = await methods.triggerEvent({ name: 'onClose' });
if (response.success === false) return;
triggerSetOpen({ state: false, setOpen, methods, rename });
if (response.bounced !== true) {
triggerSetOpen({ state: false, setOpen, methods, rename });
}
})
}
drawerStyle={methods.makeCssClass(properties.drawerStyle, { styleObjectOnly: true })}

View File

@ -56,12 +56,16 @@ const ModalBlock = ({ blockId, content, properties, events, methods }) => {
onOk={async () => {
const response = await methods.triggerEvent({ name: 'onOk' });
if (response.success === false) return;
triggerSetOpen({ state: false, setOpen, methods });
if (response.bounced !== true) {
triggerSetOpen({ state: false, setOpen, methods });
}
}}
onCancel={async () => {
const response = await methods.triggerEvent({ name: 'onCancel' });
if (response.success === false) return;
triggerSetOpen({ state: false, setOpen, methods });
if (response.bounced !== true) {
triggerSetOpen({ state: false, setOpen, methods });
}
}}
afterClose={() => methods.triggerEvent({ name: 'afterClose' })}
confirmLoading={get(events, 'onOk.loading')}

View File

@ -97,19 +97,23 @@ _ref:
When events are triggered, the can provide a data object describing the event (e.g. a description of the clicked item or uploaded file). This data object can be accessed using the [`_event`](/_event) operator in an action definition.
# Catching action errors
If one action in the chain of event actions fails by throwing an error, the actions in the list following the failed action will not be executed. To handle any errors thrown by an action, Lowdefy event actions can be provided as lists of `try` and `catch` actions.
The schema for passing actions to Lowdefy events is:
```
(eventName: action[])
(eventName: {
debounce?: {
ms?: number,
immediate?: boolean,
},
try: action[],
catch?: action[],
})
```
# Catching action errors
If one action in the chain of event actions fails by throwing an error, the actions in the list following the failed action will not be executed. To handle any errors thrown by an action, Lowdefy event actions can be provided as lists of `try` and `catch` actions.
###### Event try catch actions example for dealing with action errors:
```yaml
- id: block_with_actions
@ -132,6 +136,51 @@ _ref:
# ...
```
# Debouncing events
Event debouncing can be turned on by setting the `debounce` field on event objects. If `debounce.immediate` is `true`, leading edge debouncing or throttling will apply, else it will be debounced as trailing edge.
To control the debounce delay, set `debounce.ms` to the number of milliseconds to delay. The default delay is 300 milliseconds. If an event is triggered within that time, the event will not be triggered again. See [debounce vs throttling](https://redd.one/blog/debounce-vs-throttle) for a more detailed explanation.
###### Event trailing edge debouncing example:
```yaml
- id: block_with_actions
type: Block
properties:
# ...
events:
onEvent1:
debounce:
ms: 1000
try:
- id: action1
type: ActionType1
params:
# ...
- id: action2
type: ActionType2
```
###### Event throttling or leading edge debouncing example:
```yaml
- id: block_with_actions
type: Block
properties:
# ...
events:
onEvent1:
debounce:
ms: 1000
immediate: true
try:
- id: action1
type: ActionType1
params:
# ...
- id: action2
type: ActionType2
```
# Context initialisation events
Four events are always defined for [`context`](/context) type blocks, called in the following order:

View File

@ -90,33 +90,36 @@ class Actions {
console.error(errorCatch);
return {
blockId: block.blockId,
bounced: false,
endTimestamp: new Date(),
error,
errorCatch,
event,
eventName,
responses,
endTimestamp: new Date(),
startTimestamp,
success: false,
};
}
return {
blockId: block.blockId,
bounced: false,
endTimestamp: new Date(),
error,
event,
eventName,
responses,
endTimestamp: new Date(),
startTimestamp,
success: false,
};
}
return {
blockId: block.blockId,
bounced: false,
endTimestamp: new Date(),
event,
eventName,
responses,
endTimestamp: new Date(),
startTimestamp,
success: true,
};

View File

@ -18,7 +18,9 @@ import { type } from '@lowdefy/helpers';
class Events {
constructor({ arrayIndices, block, context }) {
this.defaultDebounceMs = 300;
this.events = {};
this.timeouts = {};
this.arrayIndices = arrayIndices;
this.block = block;
this.context = context;
@ -35,6 +37,7 @@ class Events {
return {
actions: (type.isObject(actions) ? actions.try : actions) || [],
catchActions: (type.isObject(actions) ? actions.catch : []) || [],
debounce: type.isObject(actions) ? actions.debounce : null,
history: [],
loading: false,
};
@ -52,37 +55,82 @@ class Events {
async triggerEvent({ name, event }) {
const eventDescription = this.events[name];
let result = {
blockId: this.block.blockId,
event,
eventName: name,
responses: {},
endTimestamp: new Date(),
startTimestamp: new Date(),
success: true,
bounced: false,
};
// no event
if (type.isUndefined(eventDescription)) {
return {
blockId: this.block.blockId,
event,
eventName: name,
responses: {},
endTimestamp: new Date(),
startTimestamp: new Date(),
success: true,
};
return result;
}
eventDescription.loading = true;
this.block.update = true;
this.context.update();
const result = await this.context.Actions.callActions({
actions: eventDescription.actions,
arrayIndices: this.arrayIndices,
block: this.block,
catchActions: eventDescription.catchActions,
event,
eventName: name,
const actionHandle = async () => {
const res = await this.context.Actions.callActions({
actions: eventDescription.actions,
arrayIndices: this.arrayIndices,
block: this.block,
catchActions: eventDescription.catchActions,
event,
eventName: name,
});
eventDescription.history.unshift(res);
this.context.eventLog.unshift(res);
eventDescription.loading = false;
this.block.update = true;
this.context.update();
return res;
};
// no debounce
if (type.isNone(eventDescription.debounce)) {
return actionHandle();
}
const delay = !type.isNone(eventDescription.debounce.ms)
? eventDescription.debounce.ms
: this.defaultDebounceMs;
// leading edge: bounce
if (this.timeouts[name] && eventDescription.debounce.immediate === true) {
result.bounced = true;
eventDescription.history.unshift(result);
this.context.eventLog.unshift(result);
return result;
}
// leading edge: trigger
if (eventDescription.debounce.immediate === true) {
this.timeouts[name] = setTimeout(() => {
this.timeouts[name] = null;
}, delay);
return actionHandle();
}
// trailing edge
if (eventDescription.bouncer) {
eventDescription.bouncer();
}
return new Promise((resolve) => {
const timeout = setTimeout(async () => {
eventDescription.bouncer = null;
const res = await actionHandle();
resolve(res);
}, delay);
eventDescription.bouncer = () => {
clearTimeout(timeout);
result.bounced = true;
eventDescription.history.unshift(result);
this.context.eventLog.unshift(result);
resolve(result);
};
});
eventDescription.history.unshift(result);
this.context.eventLog.unshift(result);
eventDescription.loading = false;
this.block.update = true;
this.context.update();
return result;
}
}

View File

@ -93,6 +93,7 @@ test('call a synchronous action', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
responses: {
@ -131,6 +132,7 @@ test('call a asynchronous action', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
responses: {
@ -172,6 +174,7 @@ test('call 2 actions', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
responses: {
@ -483,6 +486,7 @@ test('skip a action', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
responses: {
@ -521,6 +525,7 @@ test('action throws a error', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
error: {
@ -574,6 +579,7 @@ test('actions after a error are not called throws a error', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
error: {
@ -625,6 +631,7 @@ test('Invalid action type', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
error: {
@ -674,6 +681,7 @@ test('Parser error in action', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
error: {
@ -968,6 +976,7 @@ test('Call catchActions when actions throws error', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
endTimestamp: {
date: 0,
},
@ -1051,6 +1060,7 @@ test('Call catchActions when actions throws error and catchActions throws error'
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
endTimestamp: {
date: 0,
},
@ -1134,6 +1144,7 @@ test('call 2 actions, first with async: true', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
responses: {
@ -1150,6 +1161,7 @@ test('call 2 actions, first with async: true', async () => {
await timeout(110);
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
responses: {
@ -1195,6 +1207,7 @@ test('call async: true with error', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
responses: {
@ -1211,6 +1224,7 @@ test('call async: true with error', async () => {
await timeout(110);
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
responses: {
@ -1256,6 +1270,7 @@ test('call 2 actions, first with async: false', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
responses: {
@ -1301,6 +1316,7 @@ test('call 2 actions, first with async: null', async () => {
});
expect(res).toEqual({
blockId: 'blockId',
bounced: false,
event: {},
eventName: 'eventName',
responses: {

View File

@ -85,6 +85,7 @@ test('CallMethod with no args, synchronous method', async () => {
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -161,6 +162,7 @@ test('CallMethod method return a promise', async () => {
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -229,6 +231,7 @@ test('CallMethod with args not an array', async () => {
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {
@ -315,6 +318,7 @@ test('CallMethod with multiple positional args, synchronous method', async () =>
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -535,6 +539,7 @@ test('CallMethod with method does not exist', async () => {
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {

View File

@ -79,6 +79,7 @@ test('JsAction with no args, synchronous fn', async () => {
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -145,6 +146,7 @@ test('JsAction with no args, async fn', async () => {
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -206,6 +208,7 @@ test('JsAction with args, synchronous fn', async () => {
expect(res).toMatchInlineSnapshot(`
Object {
"blockId": "button",
"bounced": false,
"endTimestamp": Object {
"date": 0,
},
@ -298,6 +301,7 @@ test('JsAction name not a string', async () => {
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {
@ -364,11 +368,11 @@ test('JsAction args not an array', async () => {
lowdefy,
rootBlock,
});
const mockFn = jest.fn(() => 'js_fn');
const { button } = context.RootBlocks.map;
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {
@ -437,11 +441,11 @@ test('JsAction args not a function', async () => {
lowdefy,
rootBlock,
});
const mockFn = jest.fn(() => 'js_fn');
const { button } = context.RootBlocks.map;
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {

View File

@ -163,6 +163,7 @@ test('Link error', async () => {
]);
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {

View File

@ -141,6 +141,7 @@ test('Request call one request', async () => {
});
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -221,6 +222,7 @@ test('Request call all requests', async () => {
});
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -301,6 +303,7 @@ test('Request call array of requests', async () => {
});
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -400,6 +403,7 @@ test('Request call request error', async () => {
});
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {
@ -469,6 +473,7 @@ test('Request call request graphql error', async () => {
});
expect(res).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {

View File

@ -112,6 +112,7 @@ test('RestValidation after required field', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {
@ -155,6 +156,7 @@ test('RestValidation after required field', async () => {
await reset.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
endTimestamp: {
date: 0,
},

View File

@ -92,6 +92,7 @@ test('Throw no params', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -153,6 +154,7 @@ test('Throw throw true no message or metaData', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
error: {
error: {
type: 'Throw',
@ -236,6 +238,7 @@ test('Throw throw true message no metaData', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
error: {
error: {
type: 'Throw',
@ -319,6 +322,7 @@ test('Throw throw true message metaData string', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
error: {
error: {
type: 'Throw',
@ -410,6 +414,7 @@ test('Throw throw true message metaData object', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
error: {
error: {
type: 'Throw',
@ -501,6 +506,7 @@ test('Throw throw false', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -562,6 +568,7 @@ test('Throw throw invalid', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
error: {
error: {
type: 'Throw',

View File

@ -97,6 +97,7 @@ test('Validate required field', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {
@ -143,6 +144,7 @@ test('Validate required field', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -235,6 +237,7 @@ test('Validate all fields', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {
@ -286,6 +289,7 @@ test('Validate all fields', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {
@ -337,6 +341,7 @@ test('Validate all fields', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -433,6 +438,7 @@ test('Validate only one field', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {
@ -485,6 +491,7 @@ test('Validate only one field', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -606,6 +613,7 @@ test('Validate list of fields', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {
@ -663,6 +671,7 @@ test('Validate list of fields', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -721,6 +730,7 @@ test('Invalid Validate params', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
error: {
@ -815,6 +825,7 @@ test('Validate does not fail on warnings', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: undefined,
eventName: 'onClick',
responses: {
@ -906,6 +917,7 @@ test('Validate on nested objects using params.regex string', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
error: {
error: { type: 'Validate', error: new Error('Your input has 1 validation error.'), index: 0 },
action: { id: 'validate', type: 'Validate', params: { regex: '^obj.*1$' } },
@ -1015,6 +1027,7 @@ test('Validate on nested objects using params.regex array', async () => {
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
error: {
error: {
type: 'Validate',
@ -1139,6 +1152,7 @@ test('Validate on nested objects using params.regex array and blockIds', async (
await button.triggerEvent({ name: 'onClick' });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
error: {
error: {
type: 'Validate',

View File

@ -132,6 +132,7 @@ test('Wait ms not a integer', async () => {
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
bounced: false,
endTimestamp: { date: 0 },
error: {
action: { id: 'a', params: { ms: 1.1 }, type: 'Wait' },

View File

@ -57,6 +57,10 @@ const lowdefy = {
pageId,
};
const timeout = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
// Comment out to use console
console.log = () => {};
console.error = () => {};
@ -105,10 +109,11 @@ test('init Events', async () => {
const { button } = context.RootBlocks.map;
expect(button.Events.events).toEqual({
onClick: {
actions: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
catchActions: [],
debounce: null,
history: [],
loading: false,
catchActions: [],
actions: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
},
});
});
@ -144,6 +149,7 @@ test('triggerEvent no event defined', async () => {
const res = await promise;
expect(res).toEqual({
blockId: 'button',
bounced: false,
endTimestamp: { date: 0 },
event: undefined,
eventName: 'onClick',
@ -185,15 +191,17 @@ test('triggerEvent x1', async () => {
const promise = button.triggerEvent({ name: 'onClick', event: { x: 1 } });
expect(button.Events.events).toEqual({
onClick: {
actions: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
catchActions: [],
debounce: null,
history: [],
loading: true,
catchActions: [],
actions: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
},
});
await promise;
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: {
x: 1,
},
@ -290,6 +298,7 @@ test('triggerEvent error', async () => {
await button.triggerEvent({ name: 'onClick', event: { x: 1 } });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: {
x: 1,
},
@ -353,15 +362,17 @@ test('registerEvent then triggerEvent x1', async () => {
});
expect(button.Events.events).toEqual({
onClick: {
actions: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
catchActions: [],
debounce: null,
history: [],
loading: false,
catchActions: [],
actions: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
},
});
await button.triggerEvent({ name: 'onClick', event: { x: 1 } });
expect(button.Events.events.onClick.history[0]).toEqual({
blockId: 'button',
bounced: false,
event: {
x: 1,
},
@ -424,9 +435,11 @@ test('triggerEvent skip', async () => {
},
],
"catchActions": Array [],
"debounce": null,
"history": Array [
Object {
"blockId": "button",
"bounced": false,
"endTimestamp": Object {
"date": 0,
},
@ -455,6 +468,7 @@ test('triggerEvent skip', async () => {
Array [
Object {
"blockId": "button",
"bounced": false,
"endTimestamp": Object {
"date": 0,
},
@ -523,9 +537,11 @@ test('triggerEvent skip tests === true', async () => {
},
],
"catchActions": Array [],
"debounce": null,
"history": Array [
Object {
"blockId": "button",
"bounced": false,
"endTimestamp": Object {
"date": 0,
},
@ -554,6 +570,7 @@ test('triggerEvent skip tests === true', async () => {
Array [
Object {
"blockId": "button",
"bounced": false,
"endTimestamp": Object {
"date": 0,
},
@ -611,8 +628,8 @@ test('Actions array defaults', async () => {
actions: null,
});
expect(button.Events.events).toEqual({
onClick: { actions: [], history: [], loading: false, catchActions: [] },
registered: { actions: [], history: [], loading: false, catchActions: [] },
onClick: { actions: [], history: [], loading: false, catchActions: [], debounce: null },
registered: { actions: [], history: [], loading: false, catchActions: [], debounce: null },
});
});
@ -649,7 +666,7 @@ test('Actions try catch array defaults', async () => {
});
const { button } = context.RootBlocks.map;
expect(button.Events.events).toEqual({
onClick: { actions: [], history: [], loading: false, catchActions: [] },
onClick: { actions: [], history: [], loading: false, catchActions: [], debounce: undefined },
});
});
@ -694,3 +711,344 @@ test('Actions try catch arrays', async () => {
},
});
});
test('Actions try catch arrays and debounce.immediate == true (leading edge)', async () => {
const rootBlock = {
blockId: 'root',
meta: {
category: 'context',
},
areas: {
content: {
blocks: [
{
blockId: 'button',
type: 'Button',
meta: {
category: 'display',
valueType: 'string',
},
events: {
onClick: {
debounce: {
ms: 100,
immediate: true,
},
try: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
},
},
},
],
},
},
};
const context = await testContext({
lowdefy,
rootBlock,
});
const { button } = context.RootBlocks.map;
expect(button.Events.events).toEqual({
onClick: {
actions: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
history: [],
loading: false,
catchActions: [],
debounce: {
immediate: true,
ms: 100,
},
},
});
const firstClick = button.triggerEvent({ name: 'onClick', event: { x: 1 } });
await timeout(10);
expect(context.eventLog.length).toEqual(1);
const secondClick = button.triggerEvent({ name: 'onClick', event: { x: 1 } });
expect(context.eventLog.length).toEqual(2);
const secondClickResponse = await secondClick;
expect(secondClickResponse.bounced).toEqual(true);
const firstClickResponse = await firstClick;
expect(firstClickResponse.bounced).toEqual(false);
await timeout(100);
const thirdClick = await button.triggerEvent({ name: 'onClick', event: { x: 1 } });
expect(thirdClick.bounced).toEqual(false);
expect(context.eventLog).toEqual([
{
blockId: 'button',
bounced: false,
endTimestamp: {
date: 0,
},
event: {
x: 1,
},
eventName: 'onClick',
responses: {
a: {
index: 0,
response: undefined,
type: 'SetState',
},
},
startTimestamp: {
date: 0,
},
success: true,
},
{
blockId: 'button',
bounced: true,
endTimestamp: {
date: 0,
},
event: {
x: 1,
},
eventName: 'onClick',
responses: {},
startTimestamp: {
date: 0,
},
success: true,
},
{
blockId: 'button',
bounced: false,
endTimestamp: {
date: 0,
},
event: {
x: 1,
},
eventName: 'onClick',
responses: {
a: {
index: 0,
response: undefined,
type: 'SetState',
},
},
startTimestamp: {
date: 0,
},
success: true,
},
]);
});
test('Actions try catch arrays and debounce.immediate == undefined (trailing edge)', async () => {
const rootBlock = {
blockId: 'root',
meta: {
category: 'context',
},
areas: {
content: {
blocks: [
{
blockId: 'button',
type: 'Button',
meta: {
category: 'display',
valueType: 'string',
},
events: {
onClick: {
debounce: {
ms: 100,
},
try: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
},
},
},
],
},
},
};
const context = await testContext({
lowdefy,
rootBlock,
});
const { button } = context.RootBlocks.map;
expect(button.Events.events).toEqual({
onClick: {
actions: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
history: [],
loading: false,
catchActions: [],
debounce: {
ms: 100,
},
},
});
const firstClick = button.triggerEvent({ name: 'onClick', event: { x: 1 } });
await timeout(10);
const secondClick = button.triggerEvent({ name: 'onClick', event: { x: 1 } });
const firstClickResponse = await firstClick;
expect(firstClickResponse.bounced).toEqual(true);
const secondClickResponse = await secondClick;
expect(secondClickResponse.bounced).toEqual(false);
const thirdClick = await button.triggerEvent({ name: 'onClick', event: { x: 1 } });
expect(thirdClick.bounced).toEqual(false);
expect(context.eventLog).toEqual([
{
blockId: 'button',
bounced: false,
endTimestamp: {
date: 0,
},
event: {
x: 1,
},
eventName: 'onClick',
responses: {
a: {
index: 0,
response: undefined,
type: 'SetState',
},
},
startTimestamp: {
date: 0,
},
success: true,
},
{
blockId: 'button',
bounced: false,
endTimestamp: {
date: 0,
},
event: {
x: 1,
},
eventName: 'onClick',
responses: {
a: {
index: 0,
response: undefined,
type: 'SetState',
},
},
startTimestamp: {
date: 0,
},
success: true,
},
{
blockId: 'button',
bounced: true,
endTimestamp: {
date: 0,
},
event: {
x: 1,
},
eventName: 'onClick',
responses: {},
startTimestamp: {
date: 0,
},
success: true,
},
]);
});
test('Actions try catch arrays and debounce.immediate == false default ms (trailing edge)', async () => {
const rootBlock = {
blockId: 'root',
meta: {
category: 'context',
},
areas: {
content: {
blocks: [
{
blockId: 'button',
type: 'Button',
meta: {
category: 'display',
valueType: 'string',
},
events: {
onClick: {
debounce: {
immediate: false,
},
try: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
},
},
},
],
},
},
};
const context = await testContext({
lowdefy,
rootBlock,
});
const { button } = context.RootBlocks.map;
expect(button.Events.events).toEqual({
onClick: {
actions: [{ id: 'a', type: 'SetState', params: { a: 'a' } }],
history: [],
loading: false,
catchActions: [],
debounce: {
immediate: false,
},
},
});
const firstClick = button.triggerEvent({ name: 'onClick', event: { x: 1 } });
await timeout(10);
expect(context.eventLog.length).toEqual(0);
const secondClick = button.triggerEvent({ name: 'onClick', event: { x: 1 } });
expect(context.eventLog.length).toEqual(1);
await timeout(250);
expect(context.eventLog.length).toEqual(1);
await timeout(60);
expect(context.eventLog.length).toEqual(2);
const firstClickResponse = await firstClick;
expect(firstClickResponse.bounced).toEqual(true);
const secondClickResponse = await secondClick;
expect(secondClickResponse.bounced).toEqual(false);
expect(context.eventLog).toEqual([
{
blockId: 'button',
bounced: false,
endTimestamp: {
date: 0,
},
event: {
x: 1,
},
eventName: 'onClick',
responses: {
a: {
index: 0,
response: undefined,
type: 'SetState',
},
},
startTimestamp: {
date: 0,
},
success: true,
},
{
blockId: 'button',
bounced: true,
endTimestamp: {
date: 0,
},
event: {
x: 1,
},
eventName: 'onClick',
responses: {},
startTimestamp: {
date: 0,
},
success: true,
},
]);
});