From 35adca7f8cfa7073f0547ef9c24ca414077e38a3 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrinovich <52966626+dmlvr@users.noreply.github.com> Date: Wed, 13 May 2026 16:20:24 +0300 Subject: [PATCH] T1328518 - Scrollable - Simulated scrolling breaks after an unhandled JavaScript exception (#33565) --- .../ui/scroll_view/scrollable.simulated.ts | 7 +- .../scrollable.actions.tests.js | 96 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/ui/scroll_view/scrollable.simulated.ts b/packages/devextreme/js/__internal/ui/scroll_view/scrollable.simulated.ts index 5ceec5b1c3ad..52236c88c24d 100644 --- a/packages/devextreme/js/__internal/ui/scroll_view/scrollable.simulated.ts +++ b/packages/devextreme/js/__internal/ui/scroll_view/scrollable.simulated.ts @@ -29,6 +29,7 @@ import { getHeight, getWidth } from '@js/core/utils/size'; import { isDefined } from '@js/core/utils/type'; import { getWindow, hasWindow } from '@js/core/utils/window'; import type { ScrollEvent } from '@js/ui/scroll_view'; +import { logger } from '@ts/core/utils/m_console'; import type { ActionConfig } from '@ts/core/widget/component'; import Animator from '@ts/ui/scroll_view/animator'; import type { ScrollViewScroller } from '@ts/ui/scroll_view/scroll_view.simulated'; @@ -1094,7 +1095,11 @@ export class SimulatedStrategy< const actionHandler = this._createActionByOption(optionName); return (...args: unknown[]) => { - actionHandler(extend(this._createActionArgs(), args)); + try { + actionHandler(extend(this._createActionArgs(), args)); + } catch (e) { + logger.error(e); + } }; } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/scrollableParts/scrollable.actions.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/scrollableParts/scrollable.actions.tests.js index f7a78f7e2353..c7f2f6363390 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/scrollableParts/scrollable.actions.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/scrollableParts/scrollable.actions.tests.js @@ -404,3 +404,99 @@ QUnit.test('update', function(assert) { .move(0, moveDistance) .up(); }); + +QUnit.module('actions stay functional after callback errors', moduleConfig); + +[ + { + callbackName: 'onScroll', + message: 'onScroll error does not break subsequent actions', + configure: function(callback) { + const $scrollable = $('#scrollable').dxScrollable({ + useNative: false, + inertiaEnabled: false, + bounceEnabled: false, + onScroll: callback, + }); + + return { + trigger: function() { + pointerMock($scrollable.find('.' + SCROLLABLE_CONTENT_CLASS)) + .start() + .down() + .move(0, -10) + .up(); + }, + }; + }, + }, + { + callbackName: 'onUpdated', + message: 'onUpdated error does not break subsequent actions', + configure: function(callback) { + const scrollable = $('#scrollable').dxScrollable({ + useNative: false, + onUpdated: callback, + }).dxScrollable('instance'); + + return { + trigger: function() { + scrollable.update(); + }, + }; + }, + }, + { + callbackName: 'onStart', + message: 'onStart error does not break subsequent actions', + configure: function(callback) { + const scrollable = $('#scrollable').dxScrollable({ + useNative: false, + onStart: callback, + }).dxScrollable('instance'); + + return { + trigger: function() { + scrollable.scrollBy({ top: 10 }); + }, + }; + }, + }, + { + callbackName: 'onEnd', + message: 'onEnd error does not break subsequent actions', + configure: function(callback) { + const scrollable = $('#scrollable').dxScrollable({ + useNative: false, + onEnd: callback, + }).dxScrollable('instance'); + + return { + trigger: function() { + scrollable.scrollBy({ top: 10 }); + }, + }; + }, + }, +].forEach((testCase) => { + QUnit.test(testCase.message, function(assert) { + let callbackCallCount = 0; + + const { trigger } = testCase.configure(function() { + callbackCallCount++; + throw new Error(testCase.callbackName + ' error'); + }); + + callbackCallCount = 0; + + trigger(); + trigger(); + + assert.strictEqual( + callbackCallCount, + 2, + `${testCase.callbackName}: component remained functional and handled both actions after callback errors`, + ); + }); +}); +