mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 01:26:44 +00:00
fix(editor): Add debounce to text parameter input (#19339)
This commit is contained in:
@@ -37,7 +37,8 @@ export const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
||||
.find('.parameter-input')
|
||||
.find('input')
|
||||
.clear()
|
||||
.type(webhookPath);
|
||||
.type(webhookPath)
|
||||
.wait(300);
|
||||
|
||||
if (authentication) {
|
||||
cy.getByTestId('parameter-input-authentication').click();
|
||||
@@ -46,7 +47,8 @@ export const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
||||
|
||||
if (responseCode) {
|
||||
cy.get('.param-options').click();
|
||||
getVisibleSelect().contains('Response Code').click();
|
||||
// wait for selector debounce
|
||||
getVisibleSelect().contains('Response Code').click().wait(300);
|
||||
cy.get('.parameter-item-wrapper > .parameter-input-list-wrapper').children().click();
|
||||
getVisibleSelect().contains('201').click();
|
||||
}
|
||||
|
||||
@@ -78,7 +78,8 @@ describe('n8n Form Trigger', () => {
|
||||
|
||||
//add optional submitted message
|
||||
cy.get('.param-options').click();
|
||||
getVisibleSelect().find('span').contains('Form Response').click();
|
||||
// wait for selector debounce
|
||||
getVisibleSelect().find('span').contains('Form Response').click().wait(300);
|
||||
cy.contains('span', 'Text to Show')
|
||||
.should('exist')
|
||||
.parent()
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"cypress:install": "cypress install",
|
||||
"test:e2e:ui": "scripts/run-e2e.js ui",
|
||||
"test:e2e:dev": "scripts/run-e2e.js dev",
|
||||
"test:e2e:all": "scripts/run-e2e.js all",
|
||||
"test:flaky": "scripts/run-e2e.js debugFlaky",
|
||||
"test:e2e:ui": "node scripts/run-e2e.js ui",
|
||||
"test:e2e:dev": "node scripts/run-e2e.js dev",
|
||||
"test:e2e:all": "node scripts/run-e2e.js all",
|
||||
"test:flaky": "node scripts/run-e2e.js debugFlaky",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
|
||||
@@ -74,7 +74,9 @@ export class CredentialsModal extends BasePage {
|
||||
.filter(':not([readonly])')
|
||||
.each(($el) => {
|
||||
cy.wrap($el).type('test');
|
||||
});
|
||||
})
|
||||
// wait for text input debounce
|
||||
.wait(300);
|
||||
saveCredential();
|
||||
if (closeModal) {
|
||||
this.getters.closeButton().click();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { computed, nextTick, ref } from 'vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { fireEvent } from '@testing-library/vue';
|
||||
import { fireEvent, waitFor } from '@testing-library/vue';
|
||||
import Assignment from './Assignment.vue';
|
||||
import { defaultSettings } from '@/__tests__/defaults';
|
||||
import { STORES } from '@n8n/stores';
|
||||
@@ -53,9 +53,11 @@ describe('Assignment.vue', () => {
|
||||
|
||||
await userEvent.click(baseElement.querySelectorAll('.option')[3]);
|
||||
|
||||
expect(emitted('update:model-value')[0]).toEqual([
|
||||
{ name: 'New name', type: 'array', value: 'New value' },
|
||||
]);
|
||||
await waitFor(() =>
|
||||
expect(emitted('update:model-value')[0]).toEqual([
|
||||
{ name: 'New name', type: 'array', value: 'New value' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('can remove itself', async () => {
|
||||
|
||||
@@ -221,7 +221,9 @@ describe('ParameterInput.vue', () => {
|
||||
|
||||
await userEvent.click(options[1]);
|
||||
|
||||
expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 1 })]);
|
||||
await waitFor(() =>
|
||||
expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 1 })]),
|
||||
);
|
||||
});
|
||||
|
||||
test('should render a string parameter', async () => {
|
||||
@@ -241,7 +243,9 @@ describe('ParameterInput.vue', () => {
|
||||
|
||||
await userEvent.type(input, 'foo');
|
||||
|
||||
expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 'foo' })]);
|
||||
await waitFor(() =>
|
||||
expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 'foo' })]),
|
||||
);
|
||||
});
|
||||
|
||||
describe('paste events', () => {
|
||||
@@ -269,17 +273,23 @@ describe('ParameterInput.vue', () => {
|
||||
await userEvent.click(input);
|
||||
|
||||
await paste(input, 'foo');
|
||||
expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 'foo' })]);
|
||||
await waitFor(() =>
|
||||
expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 'foo' })]),
|
||||
);
|
||||
|
||||
await paste(input, '={{ $json.foo }}');
|
||||
expect(emitted('update')).toContainEqual([
|
||||
expect.objectContaining({ value: '={{ $json.foo }}' }),
|
||||
]);
|
||||
await waitFor(() =>
|
||||
expect(emitted('update')).toContainEqual([
|
||||
expect.objectContaining({ value: '={{ $json.foo }}' }),
|
||||
]),
|
||||
);
|
||||
|
||||
await paste(input, '=flDvzj%y1nP');
|
||||
expect(emitted('update')).toContainEqual([
|
||||
expect.objectContaining({ value: '==flDvzj%y1nP' }),
|
||||
]);
|
||||
await waitFor(() =>
|
||||
expect(emitted('update')).toContainEqual([
|
||||
expect.objectContaining({ value: '==flDvzj%y1nP' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle pasting an expression into a number parameter', async () => {
|
||||
@@ -299,9 +309,11 @@ describe('ParameterInput.vue', () => {
|
||||
await userEvent.click(input);
|
||||
|
||||
await paste(input, '{{ $json.foo }}');
|
||||
expect(emitted('update')).toContainEqual([
|
||||
expect.objectContaining({ value: '={{ $json.foo }}' }),
|
||||
]);
|
||||
await waitFor(() =>
|
||||
expect(emitted('update')).toContainEqual([
|
||||
expect.objectContaining({ value: '={{ $json.foo }}' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -603,4 +615,41 @@ describe('ParameterInput.vue', () => {
|
||||
await waitFor(() => expect(expressionEditorInput).not.toHaveFocus());
|
||||
});
|
||||
});
|
||||
|
||||
describe('debounced input', () => {
|
||||
test('should debounce text input and emit update event only once', async () => {
|
||||
const { container, emitted } = renderComponent({
|
||||
props: {
|
||||
path: 'textField',
|
||||
parameter: {
|
||||
displayName: 'Text Field',
|
||||
name: 'textField',
|
||||
type: 'string',
|
||||
},
|
||||
modelValue: '',
|
||||
},
|
||||
});
|
||||
|
||||
const input = container.querySelector('input') as HTMLInputElement;
|
||||
expect(input).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
await userEvent.type(input, 'h');
|
||||
await userEvent.type(input, 'e');
|
||||
await userEvent.type(input, 'l');
|
||||
await userEvent.type(input, 'l');
|
||||
await userEvent.type(input, 'o');
|
||||
// by now the update event should not have been emitted because of debouncing
|
||||
expect(emitted('update')).not.toContainEqual([expect.objectContaining({ value: 'hello' })]);
|
||||
|
||||
// Now the update event should have been emitted
|
||||
await waitFor(() => {
|
||||
const updateEvents = emitted('update');
|
||||
expect(updateEvents).toBeDefined();
|
||||
expect(updateEvents.length).toBeLessThan(5);
|
||||
expect(updateEvents).toContainEqual([expect.objectContaining({ value: 'hello' })]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1010,6 +1010,8 @@ function onUpdateTextInput(value: string) {
|
||||
onTextInputChange(value);
|
||||
}
|
||||
|
||||
const onUpdateTextInputDebounced = debounce(onUpdateTextInput, { debounceTime: 200 });
|
||||
|
||||
function onClickOutsideMapper() {
|
||||
if (!isFocused.value) {
|
||||
isMapperShown.value = false;
|
||||
@@ -1150,7 +1152,8 @@ defineExpose({
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
valueChangedDebounced.cancel();
|
||||
valueChangedDebounced.flush();
|
||||
onUpdateTextInputDebounced.flush();
|
||||
props.eventBus.off('optionSelected', optionSelected);
|
||||
});
|
||||
|
||||
@@ -1289,7 +1292,7 @@ onClickOutside(mapperElRef, onClickOutsideMapper);
|
||||
:node="node"
|
||||
:path="path"
|
||||
:event-bus="eventBus"
|
||||
@update:model-value="valueChanged"
|
||||
@update:model-value="valueChangedDebounced"
|
||||
@modal-opener-click="openExpressionEditorModal"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@@ -1309,7 +1312,7 @@ onClickOutside(mapperElRef, onClickOutsideMapper);
|
||||
:path="path"
|
||||
:parameter-issues="getIssues"
|
||||
:is-read-only="isReadOnly"
|
||||
@update:model-value="valueChanged"
|
||||
@update:model-value="valueChangedDebounced"
|
||||
@modal-opener-click="openExpressionEditorModal"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@@ -1574,7 +1577,6 @@ onClickOutside(mapperElRef, onClickOutsideMapper);
|
||||
:rows="editorRows"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<N8nInput
|
||||
v-else
|
||||
ref="inputField"
|
||||
@@ -1591,7 +1593,7 @@ onClickOutside(mapperElRef, onClickOutsideMapper);
|
||||
:title="displayTitle"
|
||||
:placeholder="getPlaceholder()"
|
||||
data-test-id="parameter-input-field"
|
||||
@update:model-value="(valueChanged($event) as undefined) && onUpdateTextInput($event)"
|
||||
@update:model-value="onUpdateTextInputDebounced($event)"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@@ -1637,7 +1639,7 @@ onClickOutside(mapperElRef, onClickOutsideMapper);
|
||||
type="text"
|
||||
:disabled="isReadOnly"
|
||||
:title="displayTitle"
|
||||
@update:model-value="valueChanged"
|
||||
@update:model-value="valueChangedDebounced"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createTestingPinia } from '@pinia/testing';
|
||||
import { ref } from 'vue';
|
||||
import { createAppModals } from '@/__tests__/utils';
|
||||
import { STORES } from '@n8n/stores';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
|
||||
vi.mock('vue-router', () => {
|
||||
return {
|
||||
@@ -107,15 +108,16 @@ describe('ParameterOverrideSelectableList', () => {
|
||||
});
|
||||
|
||||
await userEvent.type(getByTestId('parameter-input-field'), '2');
|
||||
expect(model.value.extraPropValues.description).toBe('Test description2');
|
||||
expect(emitted('update')).toHaveLength(1);
|
||||
expect(emitted('update')[0]).toEqual([
|
||||
{
|
||||
name: 'parameters.workflowInputs.value["test"]',
|
||||
value:
|
||||
"={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('test', `Test description2`, 'string') }}",
|
||||
},
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(model.value.extraPropValues.description).toBe('Test description2');
|
||||
expect(emitted('update')).toContainEqual([
|
||||
{
|
||||
name: 'parameters.workflowInputs.value["test"]',
|
||||
value:
|
||||
"={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('test', `Test description2`, 'string') }}",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset extra prop back to default when removed', async () => {
|
||||
|
||||
Reference in New Issue
Block a user