From 7f828cf8243547c7c3644cfea016ff9a73d92fcf Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Tue, 30 Dec 2025 10:36:57 +0100 Subject: [PATCH 01/13] fix: fix bug --- .../__tests__/workspace.base.test.ts | 72 ------- .../__tests__/workspace.recalculation.test.ts | 79 ------- .../scheduler/__tests__/workspace.test.ts | 198 ++++++++++++++++++ .../scheduler/workspaces/m_work_space.ts | 19 +- 4 files changed, 211 insertions(+), 157 deletions(-) delete mode 100644 packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts delete mode 100644 packages/devextreme/js/__internal/scheduler/__tests__/workspace.recalculation.test.ts create mode 100644 packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts deleted file mode 100644 index 297f343f1769..000000000000 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - describe, expect, it, jest, -} from '@jest/globals'; - -import { getResourceManagerMock } from '../__mock__/resource_manager.mock'; -import SchedulerTimelineDay from '../workspaces/m_timeline_day'; -import SchedulerTimelineMonth from '../workspaces/m_timeline_month'; -import SchedulerTimelineWeek from '../workspaces/m_timeline_week'; -import SchedulerTimelineWorkWeek from '../workspaces/m_timeline_work_week'; -import type SchedulerWorkSpace from '../workspaces/m_work_space'; -import SchedulerWorkSpaceDay from '../workspaces/m_work_space_day'; -import SchedulerWorkSpaceMonth from '../workspaces/m_work_space_month'; -import SchedulerWorkSpaceWeek from '../workspaces/m_work_space_week'; -import SchedulerWorkSpaceWorkWeek from '../workspaces/m_work_space_work_week'; - -type WorkspaceConstructor = new (container: Element, options?: any) => T; - -const createWorkspace = ( - WorkSpace: WorkspaceConstructor, - currentView: string, -): T => { - const container = document.createElement('div'); - const workspace = new WorkSpace(container, { - views: [currentView], - currentView, - currentDate: new Date(2017, 4, 25), - firstDayOfWeek: 0, - getResourceManager: () => getResourceManagerMock([]), - }); - (workspace as any)._isVisible = () => true; - expect(container.classList).toContain('dx-scheduler-work-space'); - - return workspace; -}; -const workSpaces: { - currentView: string; - WorkSpace: WorkspaceConstructor; -}[] = [ - { currentView: 'day', WorkSpace: SchedulerWorkSpaceDay }, - { currentView: 'week', WorkSpace: SchedulerWorkSpaceWeek }, - { currentView: 'workWeek', WorkSpace: SchedulerWorkSpaceWorkWeek }, - { currentView: 'month', WorkSpace: SchedulerWorkSpaceMonth }, - { currentView: 'timelineDay', WorkSpace: SchedulerTimelineDay }, - { currentView: 'timelineWeek', WorkSpace: SchedulerTimelineWeek }, - { currentView: 'timelineWorkWeek', WorkSpace: SchedulerTimelineWorkWeek }, - { currentView: 'timelineMonth', WorkSpace: SchedulerTimelineMonth }, -]; - -describe('scheduler workspace', () => { - workSpaces.forEach(({ currentView, WorkSpace }) => { - it(`should clear cache on dimension change, view: ${currentView}`, () => { - const workspace = createWorkspace(WorkSpace, currentView); - jest.spyOn(workspace.cache, 'clear'); - - workspace.cache.memo('test', () => 'value'); - workspace._dimensionChanged(); - - expect(workspace.cache.clear).toHaveBeenCalledTimes(1); - }); - - it(`should clear cache on _cleanView call, view: ${currentView}`, () => { - const workspace = createWorkspace(WorkSpace, currentView); - jest.spyOn(workspace.cache, 'clear'); - - workspace.cache.memo('test', () => 'value'); - workspace._cleanView(); - - expect(workspace.cache.clear).toHaveBeenCalledTimes(1); - expect(workspace.cache.size).toBe(0); - }); - }); -}); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.recalculation.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.recalculation.test.ts deleted file mode 100644 index 13ffa2259716..000000000000 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.recalculation.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - afterEach, beforeEach, describe, expect, it, -} from '@jest/globals'; -import $ from '@js/core/renderer'; - -import fx from '../../../common/core/animation/fx'; -import CustomStore from '../../../data/custom_store'; -import { createScheduler } from './__mock__/create_scheduler'; -import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; - -const CLASSES = { - scheduler: 'dx-scheduler', - workSpace: 'dx-scheduler-work-space', -}; - -describe('Workspace Recalculation with Async Templates (T661335)', () => { - beforeEach(() => { - fx.off = true; - setupSchedulerTestEnvironment({ height: 600 }); - }); - - afterEach(() => { - const $scheduler = $(document.querySelector(`.${CLASSES.scheduler}`)); - // @ts-expect-error - $scheduler.dxScheduler('dispose'); - document.body.innerHTML = ''; - fx.off = false; - }); - - it('should not duplicate workspace elements when resources are loaded asynchronously (T661335)', async () => { - const { scheduler, container } = await createScheduler({ - templatesRenderAsynchronously: true, - currentView: 'day', - views: ['day'], - groups: ['owner'], - resources: [ - { - fieldExpr: 'owner', - dataSource: [{ id: 1, text: 'Owner 1' }], - }, - { - fieldExpr: 'room', - dataSource: new CustomStore({ - load(): Promise { - return new Promise((resolve) => { - setTimeout(() => { - resolve([{ id: 1, text: 'Room 1', color: '#ff0000' }]); - }); - }); - }, - }), - }, - ], - dataSource: [ - { - text: 'Meeting in Room 1', - startDate: new Date(2017, 4, 25, 9, 0), - endDate: new Date(2017, 4, 25, 10, 0), - roomId: 1, - }, - ], - startDayHour: 9, - currentDate: new Date(2017, 4, 25), - height: 600, - }); - - scheduler.option('groups', ['room']); - - await new Promise((r) => { setTimeout(r); }); - - const $workSpaces = $(container).find(`.${CLASSES.workSpace}`); - const $groupHeader = $(container).find('.dx-scheduler-group-header'); - - expect($workSpaces.length).toBe(1); - - expect($groupHeader.length).toBeGreaterThan(0); - expect($groupHeader.text()).toContain('Room 1'); - }); -}); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts new file mode 100644 index 000000000000..4934ad1ecd94 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts @@ -0,0 +1,198 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import $ from '@js/core/renderer'; +import { getWidth } from '@js/core/utils/size'; + +import fx from '../../../common/core/animation/fx'; +import CustomStore from '../../../data/custom_store'; +import { getResourceManagerMock } from '../__mock__/resource_manager.mock'; +import SchedulerTimelineDay from '../workspaces/m_timeline_day'; +import SchedulerTimelineMonth from '../workspaces/m_timeline_month'; +import SchedulerTimelineWeek from '../workspaces/m_timeline_week'; +import SchedulerTimelineWorkWeek from '../workspaces/m_timeline_work_week'; +import type SchedulerWorkSpace from '../workspaces/m_work_space'; +import SchedulerWorkSpaceDay from '../workspaces/m_work_space_day'; +import SchedulerWorkSpaceMonth from '../workspaces/m_work_space_month'; +import SchedulerWorkSpaceWeek from '../workspaces/m_work_space_week'; +import SchedulerWorkSpaceWorkWeek from '../workspaces/m_work_space_work_week'; +import { createScheduler } from './__mock__/create_scheduler'; +import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; + +const CLASSES = { + scheduler: 'dx-scheduler', + workSpace: 'dx-scheduler-work-space', +}; + +type WorkspaceConstructor = new (container: Element, options?: any) => T; + +const createWorkspace = ( + WorkSpace: WorkspaceConstructor, + currentView: string, +): T => { + const container = document.createElement('div'); + const workspace = new WorkSpace(container, { + views: [currentView], + currentView, + currentDate: new Date(2017, 4, 25), + firstDayOfWeek: 0, + getResourceManager: () => getResourceManagerMock([]), + }); + (workspace as any)._isVisible = () => true; + expect(container.classList).toContain('dx-scheduler-work-space'); + + return workspace; +}; + +const workSpaces: { + currentView: string; + WorkSpace: WorkspaceConstructor; +}[] = [ + { currentView: 'day', WorkSpace: SchedulerWorkSpaceDay }, + { currentView: 'week', WorkSpace: SchedulerWorkSpaceWeek }, + { currentView: 'workWeek', WorkSpace: SchedulerWorkSpaceWorkWeek }, + { currentView: 'month', WorkSpace: SchedulerWorkSpaceMonth }, + { currentView: 'timelineDay', WorkSpace: SchedulerTimelineDay }, + { currentView: 'timelineWeek', WorkSpace: SchedulerTimelineWeek }, + { currentView: 'timelineWorkWeek', WorkSpace: SchedulerTimelineWorkWeek }, + { currentView: 'timelineMonth', WorkSpace: SchedulerTimelineMonth }, +]; + +describe('Workspace', () => { + describe('Base functionality', () => { + workSpaces.forEach(({ currentView, WorkSpace }) => { + it(`should clear cache on dimension change, view: ${currentView}`, () => { + const workspace = createWorkspace(WorkSpace, currentView); + jest.spyOn(workspace.cache, 'clear'); + + workspace.cache.memo('test', () => 'value'); + workspace._dimensionChanged(); + + expect(workspace.cache.clear).toHaveBeenCalledTimes(1); + }); + + it(`should clear cache on _cleanView call, view: ${currentView}`, () => { + const workspace = createWorkspace(WorkSpace, currentView); + jest.spyOn(workspace.cache, 'clear'); + + workspace.cache.memo('test', () => 'value'); + workspace._cleanView(); + + expect(workspace.cache.clear).toHaveBeenCalledTimes(1); + expect(workspace.cache.size).toBe(0); + }); + }); + }); + + describe('Recalculation with Async Templates', () => { + beforeEach(() => { + fx.off = true; + setupSchedulerTestEnvironment({ height: 600 }); + }); + + afterEach(() => { + const $scheduler = $(document.querySelector(`.${CLASSES.scheduler}`)); + // @ts-expect-error + $scheduler.dxScheduler('dispose'); + document.body.innerHTML = ''; + fx.off = false; + }); + + it('should not duplicate workspace elements when resources are loaded asynchronously (T661335)', async () => { + const { scheduler, container } = await createScheduler({ + templatesRenderAsynchronously: true, + currentView: 'day', + views: ['day'], + groups: ['owner'], + resources: [ + { + fieldExpr: 'owner', + dataSource: [{ id: 1, text: 'Owner 1' }], + }, + { + fieldExpr: 'room', + dataSource: new CustomStore({ + load(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve([{ id: 1, text: 'Room 1', color: '#ff0000' }]); + }); + }); + }, + }), + }, + ], + dataSource: [ + { + text: 'Meeting in Room 1', + startDate: new Date(2017, 4, 25, 9, 0), + endDate: new Date(2017, 4, 25, 10, 0), + roomId: 1, + }, + ], + startDayHour: 9, + currentDate: new Date(2017, 4, 25), + height: 600, + }); + + scheduler.option('groups', ['room']); + + await new Promise((r) => { setTimeout(r); }); + + const $workSpaces = $(container).find(`.${CLASSES.workSpace}`); + const $groupHeader = $(container).find('.dx-scheduler-group-header'); + + expect($workSpaces.length).toBe(1); + + expect($groupHeader.length).toBeGreaterThan(0); + expect($groupHeader.text()).toContain('Room 1'); + }); + }); + + describe('scrollTo', () => { + beforeEach(() => { + fx.off = true; + setupSchedulerTestEnvironment({ height: 600 }); + }); + + afterEach(() => { + document.body.innerHTML = ''; + fx.off = false; + }); + + it('should scroll to date with offset (T1310544)', async () => { + const { scheduler } = await createScheduler({ + views: ['timelineDay'], + currentView: 'timelineDay', + currentDate: new Date(2021, 1, 2), + firstDayOfWeek: 0, + startDayHour: 6, + endDayHour: 18, + offset: 720, + cellDuration: 60, + height: 580, + }); + + const workspace = scheduler.getWorkSpace(); + const scrollable = workspace.getScrollable(); + const $scrollable = scrollable.$element(); + const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); + + // With offset: 720 (12 hours), cells start at 18:00 (6:00 + 12h) + // For date 22:00, this should be cell index 4 (18:00=0, 19:00=1, 20:00=2, 21:00=3, 22:00=4) + const leftCellCount = 4; + const cellWidth = workspace.getCellWidth(); + const scrollableWidth = getWidth($scrollable); + const expectedLeft = leftCellCount * cellWidth - (scrollableWidth - cellWidth) / 2; + + const targetDate = new Date(2021, 1, 2, 22, 0); + scheduler.scrollTo(targetDate, undefined, false); + + expect(scrollBySpy).toHaveBeenCalledTimes(1); + const scrollParams = scrollBySpy.mock.calls[0][0] as { left: number; top: number }; + expect(scrollParams.left).toBeCloseTo(expectedLeft, 1); + + scrollBySpy.mockRestore(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index c1649f57b664..aed9cc16935c 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -1411,13 +1411,16 @@ class SchedulerWorkSpace extends Widget { const currentDate = date || new Date(this.option('currentDate')); const startDayHour = this.option('startDayHour'); const endDayHour = this.option('endDayHour'); + const viewOffset = this.option('viewOffset'); - if (hours < startDayHour) { - hours = startDayHour; - } + if (viewOffset === 0) { + if (hours < startDayHour) { + hours = startDayHour; + } - if (hours >= endDayHour) { - hours = endDayHour - 1; + if (hours >= endDayHour) { + hours = endDayHour - 1; + } } currentDate.setHours(hours, minutes, 0, 0); @@ -1865,10 +1868,14 @@ class SchedulerWorkSpace extends Widget { } _isValidScrollDate(date, throwWarning = true) { + const viewOffset = this.option('viewOffset') as number; const min = this.getStartViewDate(); const max = this.getEndViewDate(); - if (date < min || date > max) { + const extendedMin = new Date(min.getTime() - viewOffset); + const extendedMax = new Date(max.getTime() + viewOffset); + + if (date < extendedMin || date > extendedMax) { throwWarning && errors.log('W1008', date); return false; } From 37a82c0cbc9a284898895756c2f2ea93a0561d4d Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 5 Jan 2026 08:43:29 +0100 Subject: [PATCH 02/13] test: reerrange tests --- .../__tests__/workspace.base.test.ts | 72 ++++++++++++++++++ .../scheduler/__tests__/workspace.test.ts | 75 +------------------ 2 files changed, 75 insertions(+), 72 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts new file mode 100644 index 000000000000..297f343f1769 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts @@ -0,0 +1,72 @@ +import { + describe, expect, it, jest, +} from '@jest/globals'; + +import { getResourceManagerMock } from '../__mock__/resource_manager.mock'; +import SchedulerTimelineDay from '../workspaces/m_timeline_day'; +import SchedulerTimelineMonth from '../workspaces/m_timeline_month'; +import SchedulerTimelineWeek from '../workspaces/m_timeline_week'; +import SchedulerTimelineWorkWeek from '../workspaces/m_timeline_work_week'; +import type SchedulerWorkSpace from '../workspaces/m_work_space'; +import SchedulerWorkSpaceDay from '../workspaces/m_work_space_day'; +import SchedulerWorkSpaceMonth from '../workspaces/m_work_space_month'; +import SchedulerWorkSpaceWeek from '../workspaces/m_work_space_week'; +import SchedulerWorkSpaceWorkWeek from '../workspaces/m_work_space_work_week'; + +type WorkspaceConstructor = new (container: Element, options?: any) => T; + +const createWorkspace = ( + WorkSpace: WorkspaceConstructor, + currentView: string, +): T => { + const container = document.createElement('div'); + const workspace = new WorkSpace(container, { + views: [currentView], + currentView, + currentDate: new Date(2017, 4, 25), + firstDayOfWeek: 0, + getResourceManager: () => getResourceManagerMock([]), + }); + (workspace as any)._isVisible = () => true; + expect(container.classList).toContain('dx-scheduler-work-space'); + + return workspace; +}; +const workSpaces: { + currentView: string; + WorkSpace: WorkspaceConstructor; +}[] = [ + { currentView: 'day', WorkSpace: SchedulerWorkSpaceDay }, + { currentView: 'week', WorkSpace: SchedulerWorkSpaceWeek }, + { currentView: 'workWeek', WorkSpace: SchedulerWorkSpaceWorkWeek }, + { currentView: 'month', WorkSpace: SchedulerWorkSpaceMonth }, + { currentView: 'timelineDay', WorkSpace: SchedulerTimelineDay }, + { currentView: 'timelineWeek', WorkSpace: SchedulerTimelineWeek }, + { currentView: 'timelineWorkWeek', WorkSpace: SchedulerTimelineWorkWeek }, + { currentView: 'timelineMonth', WorkSpace: SchedulerTimelineMonth }, +]; + +describe('scheduler workspace', () => { + workSpaces.forEach(({ currentView, WorkSpace }) => { + it(`should clear cache on dimension change, view: ${currentView}`, () => { + const workspace = createWorkspace(WorkSpace, currentView); + jest.spyOn(workspace.cache, 'clear'); + + workspace.cache.memo('test', () => 'value'); + workspace._dimensionChanged(); + + expect(workspace.cache.clear).toHaveBeenCalledTimes(1); + }); + + it(`should clear cache on _cleanView call, view: ${currentView}`, () => { + const workspace = createWorkspace(WorkSpace, currentView); + jest.spyOn(workspace.cache, 'clear'); + + workspace.cache.memo('test', () => 'value'); + workspace._cleanView(); + + expect(workspace.cache.clear).toHaveBeenCalledTimes(1); + expect(workspace.cache.size).toBe(0); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts index 4934ad1ecd94..856c638eeef5 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts @@ -6,16 +6,6 @@ import { getWidth } from '@js/core/utils/size'; import fx from '../../../common/core/animation/fx'; import CustomStore from '../../../data/custom_store'; -import { getResourceManagerMock } from '../__mock__/resource_manager.mock'; -import SchedulerTimelineDay from '../workspaces/m_timeline_day'; -import SchedulerTimelineMonth from '../workspaces/m_timeline_month'; -import SchedulerTimelineWeek from '../workspaces/m_timeline_week'; -import SchedulerTimelineWorkWeek from '../workspaces/m_timeline_work_week'; -import type SchedulerWorkSpace from '../workspaces/m_work_space'; -import SchedulerWorkSpaceDay from '../workspaces/m_work_space_day'; -import SchedulerWorkSpaceMonth from '../workspaces/m_work_space_month'; -import SchedulerWorkSpaceWeek from '../workspaces/m_work_space_week'; -import SchedulerWorkSpaceWorkWeek from '../workspaces/m_work_space_work_week'; import { createScheduler } from './__mock__/create_scheduler'; import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; @@ -24,67 +14,8 @@ const CLASSES = { workSpace: 'dx-scheduler-work-space', }; -type WorkspaceConstructor = new (container: Element, options?: any) => T; - -const createWorkspace = ( - WorkSpace: WorkspaceConstructor, - currentView: string, -): T => { - const container = document.createElement('div'); - const workspace = new WorkSpace(container, { - views: [currentView], - currentView, - currentDate: new Date(2017, 4, 25), - firstDayOfWeek: 0, - getResourceManager: () => getResourceManagerMock([]), - }); - (workspace as any)._isVisible = () => true; - expect(container.classList).toContain('dx-scheduler-work-space'); - - return workspace; -}; - -const workSpaces: { - currentView: string; - WorkSpace: WorkspaceConstructor; -}[] = [ - { currentView: 'day', WorkSpace: SchedulerWorkSpaceDay }, - { currentView: 'week', WorkSpace: SchedulerWorkSpaceWeek }, - { currentView: 'workWeek', WorkSpace: SchedulerWorkSpaceWorkWeek }, - { currentView: 'month', WorkSpace: SchedulerWorkSpaceMonth }, - { currentView: 'timelineDay', WorkSpace: SchedulerTimelineDay }, - { currentView: 'timelineWeek', WorkSpace: SchedulerTimelineWeek }, - { currentView: 'timelineWorkWeek', WorkSpace: SchedulerTimelineWorkWeek }, - { currentView: 'timelineMonth', WorkSpace: SchedulerTimelineMonth }, -]; - describe('Workspace', () => { - describe('Base functionality', () => { - workSpaces.forEach(({ currentView, WorkSpace }) => { - it(`should clear cache on dimension change, view: ${currentView}`, () => { - const workspace = createWorkspace(WorkSpace, currentView); - jest.spyOn(workspace.cache, 'clear'); - - workspace.cache.memo('test', () => 'value'); - workspace._dimensionChanged(); - - expect(workspace.cache.clear).toHaveBeenCalledTimes(1); - }); - - it(`should clear cache on _cleanView call, view: ${currentView}`, () => { - const workspace = createWorkspace(WorkSpace, currentView); - jest.spyOn(workspace.cache, 'clear'); - - workspace.cache.memo('test', () => 'value'); - workspace._cleanView(); - - expect(workspace.cache.clear).toHaveBeenCalledTimes(1); - expect(workspace.cache.size).toBe(0); - }); - }); - }); - - describe('Recalculation with Async Templates', () => { + describe('Recalculation with Async Templates (T661335)', () => { beforeEach(() => { fx.off = true; setupSchedulerTestEnvironment({ height: 600 }); @@ -149,7 +80,7 @@ describe('Workspace', () => { }); }); - describe('scrollTo', () => { + describe('scrollTo (T1310544)', () => { beforeEach(() => { fx.off = true; setupSchedulerTestEnvironment({ height: 600 }); @@ -160,7 +91,7 @@ describe('Workspace', () => { fx.off = false; }); - it('should scroll to date with offset (T1310544)', async () => { + it('T1310544: should scroll to date with offset: 720 (12 hours)', async () => { const { scheduler } = await createScheduler({ views: ['timelineDay'], currentView: 'timelineDay', From 281fa227966ac4027dc5840b6dcb23a49dc9e95f Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Mon, 12 Jan 2026 14:45:32 +0800 Subject: [PATCH 03/13] refix --- .../scheduler/__tests__/workspace.test.ts | 176 +++++++++++++++++- .../scheduler/workspaces/m_work_space.ts | 19 +- .../view_model/m_view_data_provider.ts | 75 +++++--- 3 files changed, 226 insertions(+), 44 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts index 856c638eeef5..65f1dcc99b0c 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts @@ -80,7 +80,7 @@ describe('Workspace', () => { }); }); - describe('scrollTo (T1310544)', () => { + describe('scrollTo', () => { beforeEach(() => { fx.off = true; setupSchedulerTestEnvironment({ height: 600 }); @@ -125,5 +125,179 @@ describe('Workspace', () => { scrollBySpy.mockRestore(); }); + + it('should scroll to date with offset after midnight: 720 (12 hours)', async () => { + const { scheduler } = await createScheduler({ + views: ['timelineDay'], + currentView: 'timelineDay', + currentDate: new Date(2021, 1, 2), + firstDayOfWeek: 0, + startDayHour: 6, + endDayHour: 18, + offset: 720, + cellDuration: 60, + height: 580, + }); + + const workspace = scheduler.getWorkSpace(); + const scrollable = workspace.getScrollable(); + const $scrollable = scrollable.$element(); + const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); + + // With offset: 720 (12 hours), cells start at 18:00 (6:00 + 12h) + // For date 3 feb 04:00, this should be cell index 10 (18:00=0, 19:00=1, ... 04:00=10) + const leftCellCount = 10; + const cellWidth = workspace.getCellWidth(); + const scrollableWidth = getWidth($scrollable); + const expectedLeft = leftCellCount * cellWidth - (scrollableWidth - cellWidth) / 2; + + const targetDate = new Date(2021, 1, 3, 4, 0); + scheduler.scrollTo(targetDate, undefined, false); + + expect(scrollBySpy).toHaveBeenCalledTimes(1); + const scrollParams = scrollBySpy.mock.calls[0][0] as { left: number; top: number }; + expect(scrollParams.left).toBeCloseTo(expectedLeft, 1); + + scrollBySpy.mockRestore(); + }); + + it('should scroll to end of day', async () => { + const { scheduler } = await createScheduler({ + views: ['timelineWeek'], + currentView: 'timelineWeek', + currentDate: new Date(2021, 1, 2), + firstDayOfWeek: 0, + startDayHour: 6, + endDayHour: 18, + offset: 120, + cellDuration: 60, + height: 580, + }); + + const workspace = scheduler.getWorkSpace(); + const scrollable = workspace.getScrollable(); + const $scrollable = scrollable.$element(); + const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); + + // With offset: 720 (12 hours), cells start at 18:00 (6:00 + 12h) + // For date 3 feb 04:00, this should be cell index 3 (18:00=0, ... 22:00=35) + const leftCellCount = 35; + const cellWidth = workspace.getCellWidth(); + const scrollableWidth = getWidth($scrollable); + const expectedLeft = leftCellCount * cellWidth - (scrollableWidth - cellWidth) / 2; + + const targetDate = new Date(2021, 1, 2, 22, 0); + scheduler.scrollTo(targetDate, undefined, false); + + expect(scrollBySpy).toHaveBeenCalledTimes(1); + const scrollParams = scrollBySpy.mock.calls[0][0] as { left: number; top: number }; + expect(scrollParams.left).toBeCloseTo(expectedLeft, 1); + + scrollBySpy.mockRestore(); + }); + + describe('hour normalization', () => { + it('should normalize hours to visible range without viewOffset', async () => { + const { scheduler } = await createScheduler({ + views: ['timelineDay'], + currentView: 'timelineDay', + currentDate: new Date(2021, 1, 2), + startDayHour: 6, + endDayHour: 18, + offset: 0, + }); + + const workspace = scheduler.getWorkSpace(); + const scrollable = workspace.getScrollable(); + const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); + + // Below startDayHour (6), should NOT normalize to 6 (?) + const dateBelowRange = new Date(2021, 1, 2, 4, 0); + scheduler.scrollTo(dateBelowRange, undefined, false); + expect(scrollBySpy).not.toHaveBeenCalled(); + + scrollBySpy.mockClear(); + // Above endDayHour (18), should NOT normalize to 17 (?) + const dateAboveRange = new Date(2021, 1, 2, 20, 0); + scheduler.scrollTo(dateAboveRange, undefined, false); + expect(scrollBySpy).not.toHaveBeenCalled(); + + scrollBySpy.mockClear(); + // Within range [6, 18), should scroll normally + const dateInRange = new Date(2021, 1, 2, 12, 0); + scheduler.scrollTo(dateInRange, undefined, false); + expect(scrollBySpy).toHaveBeenCalled(); + + scrollBySpy.mockRestore(); + }); + + it('should normalize hours to visible range with viewOffset (no midnight crossing)', async () => { + const { scheduler } = await createScheduler({ + views: ['timelineDay'], + currentView: 'timelineDay', + currentDate: new Date(2021, 1, 2), + startDayHour: 6, + endDayHour: 18, + offset: 360, + }); + + const workspace = scheduler.getWorkSpace(); + const scrollable = workspace.getScrollable(); + const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); + + // Below adjustedStartDayHour (12), should NOT normalize to 12 (?) + const dateBelowAdjustedRange = new Date(2021, 1, 2, 10, 0); + scheduler.scrollTo(dateBelowAdjustedRange, undefined, false); + expect(scrollBySpy).not.toHaveBeenCalled(); + + scrollBySpy.mockClear(); + // Within adjusted range [12, 24), should scroll normally + const dateInAdjustedRange = new Date(2021, 1, 2, 15, 0); + scheduler.scrollTo(dateInAdjustedRange, undefined, false); + expect(scrollBySpy).toHaveBeenCalled(); + + scrollBySpy.mockRestore(); + }); + + it('should normalize hours to visible range with viewOffset (midnight crossing)', async () => { + const { scheduler } = await createScheduler({ + views: ['timelineDay'], + currentView: 'timelineDay', + currentDate: new Date(2021, 1, 2), + startDayHour: 6, + endDayHour: 18, + offset: 720, + }); + + const workspace = scheduler.getWorkSpace(); + const scrollable = workspace.getScrollable(); + const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); + + // In gap [6, 18), should NOT normalize to 18:00 Feb 2 (?) + const dateInGap = new Date(2021, 1, 2, 10, 0); + scheduler.scrollTo(dateInGap, undefined, false); + expect(scrollBySpy).not.toHaveBeenCalled(); + + scrollBySpy.mockClear(); + // In range [18, 24) on Feb 2, should scroll normally + const dateInFirstRange = new Date(2021, 1, 2, 22, 0); + scheduler.scrollTo(dateInFirstRange, undefined, false); + expect(scrollBySpy).toHaveBeenCalled(); + + scrollBySpy.mockClear(); + // In range [0, 6) but on wrong day (Feb 2), should NOT normalize to 18:00 Feb 2 (?) + const dateInSecondRangeWrongDay = new Date(2021, 1, 2, 3, 0); + scheduler.scrollTo(dateInSecondRangeWrongDay, undefined, false); + expect(scrollBySpy).not.toHaveBeenCalled(); + + scrollBySpy.mockClear(); + // In range [0, 6) on correct day (Feb 3), should scroll normally + const dateInSecondRangeCorrectDay = new Date(2021, 1, 3, 3, 0); + scheduler.scrollTo(dateInSecondRangeCorrectDay, undefined, false); + expect(scrollBySpy).toHaveBeenCalled(); + + scrollBySpy.mockRestore(); + }); + }); }); }); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index aed9cc16935c..46eb5389ddf2 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -1409,23 +1409,14 @@ class SchedulerWorkSpace extends Widget { _getScrollCoordinates(hours, minutes, date, groupIndex?: any, allDay?: any) { const currentDate = date || new Date(this.option('currentDate')); - const startDayHour = this.option('startDayHour'); - const endDayHour = this.option('endDayHour'); - const viewOffset = this.option('viewOffset'); - if (viewOffset === 0) { - if (hours < startDayHour) { - hours = startDayHour; - } + const cell = this.viewDataProvider.findGlobalCellPosition(currentDate, groupIndex, allDay, true); - if (hours >= endDayHour) { - hours = endDayHour - 1; - } + if (!cell) { + return; } - currentDate.setHours(hours, minutes, 0, 0); - - const cell = this.viewDataProvider.findGlobalCellPosition(currentDate, groupIndex, allDay); + currentDate.setHours(cell?.cellData.startDate.getHours(), currentDate.getMinutes(), 0, 0); return this.virtualScrollingDispatcher.calculateCoordinatesByDataAndPosition( cell?.cellData, @@ -1872,7 +1863,7 @@ class SchedulerWorkSpace extends Widget { const min = this.getStartViewDate(); const max = this.getEndViewDate(); - const extendedMin = new Date(min.getTime() - viewOffset); + const extendedMin = new Date(min.getTime() + viewOffset); const extendedMax = new Date(max.getTime() + viewOffset); if (date < extendedMin || date > extendedMax) { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts index 5f374abead1d..b1b8c0fb7907 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts @@ -282,51 +282,68 @@ export default class ViewDataProvider { return startDate < groupEndDate && endDate > groupStartDate; } - findGlobalCellPosition(date, groupIndex = 0, allDay = false) { + findGlobalCellPosition(date, groupIndex = 0, allDay = false, findClosest = false) { const { completeViewDataMap } = this; const showAllDayPanel = this._options.isAllDayPanelVisible; + let resultDiff = Number.MAX_VALUE; + let resultCellData : ViewCellData | undefined; + let resultCellColumnIndex = -1; + let resultCellRowIndex = -1; + for (let rowIndex = 0; rowIndex < completeViewDataMap.length; rowIndex += 1) { const currentRow = completeViewDataMap[rowIndex]; for (let columnIndex = 0; columnIndex < currentRow.length; columnIndex += 1) { const cellData = currentRow[columnIndex]; - const { - startDate: currentStartDate, - endDate: currentEndDate, - groupIndex: currentGroupIndex, - allDay: currentAllDay, + let { + startDate: cellStartDate, + endDate: cellEndDate, + groupIndex: cellGroupIndex, + allDay: cellAllDay, } = cellData; - if (groupIndex === currentGroupIndex - && allDay === Boolean(currentAllDay) - && this._compareDatesAndAllDay(date, currentStartDate, currentEndDate, allDay)) { - return { - position: { - columnIndex, - rowIndex: showAllDayPanel && !this._options.isVerticalGrouping - ? rowIndex - 1 - : rowIndex, - }, - cellData, - }; + if(groupIndex !== cellGroupIndex) { + continue; + } + + const isDateInCell = allDay + ? Boolean(cellAllDay) && dateUtils.sameDate(date, cellStartDate) + : Boolean(!cellAllDay) && date >= cellStartDate && date < cellEndDate; + + const diff = Math.min( + Math.abs(date.getTime() - cellStartDate.getTime()), + Math.abs(date.getTime() - cellEndDate.getTime()), + ); + + if(isDateInCell || (findClosest && diff < resultDiff)) { + console.log(cellStartDate, diff); + resultDiff = diff; + resultCellData = cellData; + resultCellColumnIndex = columnIndex; + resultCellRowIndex = rowIndex; + + if (isDateInCell) { + break; + } } } } - return undefined; - } + if(resultCellData) { + return { + position: { + columnIndex: resultCellColumnIndex, + rowIndex: showAllDayPanel && !this._options.isVerticalGrouping + ? resultCellRowIndex - 1 + : resultCellRowIndex, + }, + cellData: resultCellData, + }; + } - private _compareDatesAndAllDay( - date: Date, - cellStartDate: Date, - cellEndDate: Date, - allDay: boolean, - ): boolean { - return allDay - ? dateUtils.sameDate(date, cellStartDate) - : date >= cellStartDate && date < cellEndDate; + return undefined; } getSkippedDaysCount(groupIndex, startDate, endDate, daysCount) { From 18483a4d6c68068a1722951299d6400b45ab4f2f Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Mon, 12 Jan 2026 15:24:13 +0800 Subject: [PATCH 04/13] fix to pass tests --- .../scheduler/workspaces/m_work_space.ts | 8 ++------ .../view_model/m_view_data_provider.ts | 17 +++++++---------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index 46eb5389ddf2..4d04c1c696f4 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -1407,15 +1407,11 @@ class SchedulerWorkSpace extends Widget { return (this.$element() as any).find(`.${GROUP_HEADER_CLASS}`); } - _getScrollCoordinates(hours, minutes, date, groupIndex?: any, allDay?: any) { + _getScrollCoordinates(date, groupIndex?: any, allDay?: any) { const currentDate = date || new Date(this.option('currentDate')); const cell = this.viewDataProvider.findGlobalCellPosition(currentDate, groupIndex, allDay, true); - if (!cell) { - return; - } - currentDate.setHours(cell?.cellData.startDate.getHours(), currentDate.getMinutes(), 0, 0); return this.virtualScrollingDispatcher.calculateCoordinatesByDataAndPosition( @@ -1827,7 +1823,7 @@ class SchedulerWorkSpace extends Widget { : 0; const isScrollToAllDay = allDay && this.isAllDayPanelVisible; - const coordinates = this._getScrollCoordinates(date.getHours(), date.getMinutes(), date, groupIndex, isScrollToAllDay); + const coordinates = this._getScrollCoordinates(date, groupIndex, isScrollToAllDay); const scrollable = this.getScrollable(); const $scrollable = scrollable.$element(); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts index b1b8c0fb7907..9eab530b0589 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts @@ -304,13 +304,13 @@ export default class ViewDataProvider { allDay: cellAllDay, } = cellData; - if(groupIndex !== cellGroupIndex) { + if(groupIndex !== cellGroupIndex || allDay !== cellAllDay) { continue; } const isDateInCell = allDay - ? Boolean(cellAllDay) && dateUtils.sameDate(date, cellStartDate) - : Boolean(!cellAllDay) && date >= cellStartDate && date < cellEndDate; + ? dateUtils.sameDate(date, cellStartDate) + : date >= cellStartDate && date < cellEndDate; const diff = Math.min( Math.abs(date.getTime() - cellStartDate.getTime()), @@ -318,7 +318,6 @@ export default class ViewDataProvider { ); if(isDateInCell || (findClosest && diff < resultDiff)) { - console.log(cellStartDate, diff); resultDiff = diff; resultCellData = cellData; resultCellColumnIndex = columnIndex; @@ -331,8 +330,8 @@ export default class ViewDataProvider { } } - if(resultCellData) { - return { + return resultCellData + ? { position: { columnIndex: resultCellColumnIndex, rowIndex: showAllDayPanel && !this._options.isVerticalGrouping @@ -340,10 +339,8 @@ export default class ViewDataProvider { : resultCellRowIndex, }, cellData: resultCellData, - }; - } - - return undefined; + } + : undefined; } getSkippedDaysCount(groupIndex, startDate, endDate, daysCount) { From 52e99c2ca45961ad18586ec1c5cce658af1aa824 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Mon, 12 Jan 2026 15:25:16 +0800 Subject: [PATCH 05/13] fix lint --- .../workspaces/view_model/m_view_data_provider.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts index 9eab530b0589..ac157ad632dc 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts @@ -288,23 +288,23 @@ export default class ViewDataProvider { const showAllDayPanel = this._options.isAllDayPanelVisible; let resultDiff = Number.MAX_VALUE; - let resultCellData : ViewCellData | undefined; + let resultCellData: ViewCellData | undefined; let resultCellColumnIndex = -1; - let resultCellRowIndex = -1; + let resultCellRowIndex = -1; for (let rowIndex = 0; rowIndex < completeViewDataMap.length; rowIndex += 1) { const currentRow = completeViewDataMap[rowIndex]; for (let columnIndex = 0; columnIndex < currentRow.length; columnIndex += 1) { const cellData = currentRow[columnIndex]; - let { + const { startDate: cellStartDate, endDate: cellEndDate, groupIndex: cellGroupIndex, allDay: cellAllDay, } = cellData; - if(groupIndex !== cellGroupIndex || allDay !== cellAllDay) { + if (groupIndex !== cellGroupIndex || allDay !== cellAllDay) { continue; } @@ -317,7 +317,7 @@ export default class ViewDataProvider { Math.abs(date.getTime() - cellEndDate.getTime()), ); - if(isDateInCell || (findClosest && diff < resultDiff)) { + if (isDateInCell || (findClosest && diff < resultDiff)) { resultDiff = diff; resultCellData = cellData; resultCellColumnIndex = columnIndex; From e893f1e430be0c27c194da676f1c39deeeffff6d Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Mon, 12 Jan 2026 16:05:14 +0800 Subject: [PATCH 06/13] fix to pass ci --- .../view_model/m_view_data_provider.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts index ac157ad632dc..22d654e925a8 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts @@ -292,6 +292,13 @@ export default class ViewDataProvider { let resultCellColumnIndex = -1; let resultCellRowIndex = -1; + const getCellPosition = (rowIndex: number, cellIndex: number) => ({ + columnIndex: rowIndex, + rowIndex: showAllDayPanel && !this._options.isVerticalGrouping + ? cellIndex - 1 + : cellIndex, + }); + for (let rowIndex = 0; rowIndex < completeViewDataMap.length; rowIndex += 1) { const currentRow = completeViewDataMap[rowIndex]; @@ -304,7 +311,7 @@ export default class ViewDataProvider { allDay: cellAllDay, } = cellData; - if (groupIndex !== cellGroupIndex || allDay !== cellAllDay) { + if (groupIndex !== cellGroupIndex || allDay !== Boolean(cellAllDay)) { continue; } @@ -312,32 +319,30 @@ export default class ViewDataProvider { ? dateUtils.sameDate(date, cellStartDate) : date >= cellStartDate && date < cellEndDate; + if (isDateInCell) { + return { + position: getCellPosition(columnIndex, rowIndex), + cellData + }; + } + const diff = Math.min( Math.abs(date.getTime() - cellStartDate.getTime()), Math.abs(date.getTime() - cellEndDate.getTime()), ); - if (isDateInCell || (findClosest && diff < resultDiff)) { + if (findClosest && diff < resultDiff) { resultDiff = diff; resultCellData = cellData; resultCellColumnIndex = columnIndex; resultCellRowIndex = rowIndex; - - if (isDateInCell) { - break; - } } } } return resultCellData ? { - position: { - columnIndex: resultCellColumnIndex, - rowIndex: showAllDayPanel && !this._options.isVerticalGrouping - ? resultCellRowIndex - 1 - : resultCellRowIndex, - }, + position: getCellPosition(resultCellColumnIndex, resultCellRowIndex), cellData: resultCellData, } : undefined; From c2dff44678862d8a9023b04cda5109918ebd1b36 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Mon, 12 Jan 2026 16:43:03 +0800 Subject: [PATCH 07/13] fix lint & test --- .../js/__internal/scheduler/workspaces/m_work_space.ts | 9 +++------ .../workspaces/view_model/m_view_data_provider.ts | 7 ++----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index 4d04c1c696f4..40a255e824ac 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -1856,13 +1856,10 @@ class SchedulerWorkSpace extends Widget { _isValidScrollDate(date, throwWarning = true) { const viewOffset = this.option('viewOffset') as number; - const min = this.getStartViewDate(); - const max = this.getEndViewDate(); + const min = new Date(this.getStartViewDate().getTime() + viewOffset); + const max = new Date(this.getEndViewDate().getTime() + viewOffset); - const extendedMin = new Date(min.getTime() + viewOffset); - const extendedMax = new Date(max.getTime() + viewOffset); - - if (date < extendedMin || date > extendedMax) { + if (date < min || date > max) { throwWarning && errors.log('W1008', date); return false; } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts index 22d654e925a8..3cf33b098191 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts @@ -322,14 +322,11 @@ export default class ViewDataProvider { if (isDateInCell) { return { position: getCellPosition(columnIndex, rowIndex), - cellData + cellData, }; } - const diff = Math.min( - Math.abs(date.getTime() - cellStartDate.getTime()), - Math.abs(date.getTime() - cellEndDate.getTime()), - ); + const diff = Math.abs(date.getTime() - cellStartDate.getTime()); if (findClosest && diff < resultDiff) { resultDiff = diff; From def043fe937b61ed0402538edcd9e6b9b9c99f1e Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 12 Jan 2026 12:04:24 +0100 Subject: [PATCH 08/13] test: add test --- .../scheduler/__tests__/workspace.test.ts | 236 ++++++------------ 1 file changed, 76 insertions(+), 160 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts index 65f1dcc99b0c..d4fef5e04e75 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts @@ -126,177 +126,93 @@ describe('Workspace', () => { scrollBySpy.mockRestore(); }); - it('should scroll to date with offset after midnight: 720 (12 hours)', async () => { - const { scheduler } = await createScheduler({ - views: ['timelineDay'], - currentView: 'timelineDay', - currentDate: new Date(2021, 1, 2), - firstDayOfWeek: 0, - startDayHour: 6, - endDayHour: 18, - offset: 720, - cellDuration: 60, - height: 580, - }); - - const workspace = scheduler.getWorkSpace(); - const scrollable = workspace.getScrollable(); - const $scrollable = scrollable.$element(); - const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); - - // With offset: 720 (12 hours), cells start at 18:00 (6:00 + 12h) - // For date 3 feb 04:00, this should be cell index 10 (18:00=0, 19:00=1, ... 04:00=10) - const leftCellCount = 10; - const cellWidth = workspace.getCellWidth(); - const scrollableWidth = getWidth($scrollable); - const expectedLeft = leftCellCount * cellWidth - (scrollableWidth - cellWidth) / 2; - - const targetDate = new Date(2021, 1, 3, 4, 0); - scheduler.scrollTo(targetDate, undefined, false); - - expect(scrollBySpy).toHaveBeenCalledTimes(1); - const scrollParams = scrollBySpy.mock.calls[0][0] as { left: number; top: number }; - expect(scrollParams.left).toBeCloseTo(expectedLeft, 1); - - scrollBySpy.mockRestore(); - }); - - it('should scroll to end of day', async () => { - const { scheduler } = await createScheduler({ - views: ['timelineWeek'], - currentView: 'timelineWeek', - currentDate: new Date(2021, 1, 2), - firstDayOfWeek: 0, - startDayHour: 6, - endDayHour: 18, - offset: 120, - cellDuration: 60, - height: 580, - }); - - const workspace = scheduler.getWorkSpace(); - const scrollable = workspace.getScrollable(); - const $scrollable = scrollable.$element(); - const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); - - // With offset: 720 (12 hours), cells start at 18:00 (6:00 + 12h) - // For date 3 feb 04:00, this should be cell index 3 (18:00=0, ... 22:00=35) - const leftCellCount = 35; - const cellWidth = workspace.getCellWidth(); - const scrollableWidth = getWidth($scrollable); - const expectedLeft = leftCellCount * cellWidth - (scrollableWidth - cellWidth) / 2; - - const targetDate = new Date(2021, 1, 2, 22, 0); - scheduler.scrollTo(targetDate, undefined, false); - - expect(scrollBySpy).toHaveBeenCalledTimes(1); - const scrollParams = scrollBySpy.mock.calls[0][0] as { left: number; top: number }; - expect(scrollParams.left).toBeCloseTo(expectedLeft, 1); - - scrollBySpy.mockRestore(); - }); - describe('hour normalization', () => { - it('should normalize hours to visible range without viewOffset', async () => { - const { scheduler } = await createScheduler({ - views: ['timelineDay'], - currentView: 'timelineDay', - currentDate: new Date(2021, 1, 2), + [ + // Without offset, normal range + { startDayHour: 6, endDayHour: 18, offset: 0, - }); - - const workspace = scheduler.getWorkSpace(); - const scrollable = workspace.getScrollable(); - const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); - - // Below startDayHour (6), should NOT normalize to 6 (?) - const dateBelowRange = new Date(2021, 1, 2, 4, 0); - scheduler.scrollTo(dateBelowRange, undefined, false); - expect(scrollBySpy).not.toHaveBeenCalled(); - - scrollBySpy.mockClear(); - // Above endDayHour (18), should NOT normalize to 17 (?) - const dateAboveRange = new Date(2021, 1, 2, 20, 0); - scheduler.scrollTo(dateAboveRange, undefined, false); - expect(scrollBySpy).not.toHaveBeenCalled(); - - scrollBySpy.mockClear(); - // Within range [6, 18), should scroll normally - const dateInRange = new Date(2021, 1, 2, 12, 0); - scheduler.scrollTo(dateInRange, undefined, false); - expect(scrollBySpy).toHaveBeenCalled(); - - scrollBySpy.mockRestore(); - }); - - it('should normalize hours to visible range with viewOffset (no midnight crossing)', async () => { - const { scheduler } = await createScheduler({ - views: ['timelineDay'], - currentView: 'timelineDay', - currentDate: new Date(2021, 1, 2), + hours: [4, 12, 20], + }, + // With positive offset + { startDayHour: 6, endDayHour: 18, offset: 360, - }); - - const workspace = scheduler.getWorkSpace(); - const scrollable = workspace.getScrollable(); - const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); - - // Below adjustedStartDayHour (12), should NOT normalize to 12 (?) - const dateBelowAdjustedRange = new Date(2021, 1, 2, 10, 0); - scheduler.scrollTo(dateBelowAdjustedRange, undefined, false); - expect(scrollBySpy).not.toHaveBeenCalled(); - - scrollBySpy.mockClear(); - // Within adjusted range [12, 24), should scroll normally - const dateInAdjustedRange = new Date(2021, 1, 2, 15, 0); - scheduler.scrollTo(dateInAdjustedRange, undefined, false); - expect(scrollBySpy).toHaveBeenCalled(); - - scrollBySpy.mockRestore(); - }); - - it('should normalize hours to visible range with viewOffset (midnight crossing)', async () => { - const { scheduler } = await createScheduler({ - views: ['timelineDay'], - currentView: 'timelineDay', - currentDate: new Date(2021, 1, 2), + hours: [10, 15, 22], + }, + // With negative offset + { + startDayHour: 6, + endDayHour: 18, + offset: -120, + hours: [3, 10, 20], + }, + // With offset creating midnight crossing + { startDayHour: 6, endDayHour: 18, offset: 720, + hours: [10, 22, 3], + }, + // Edge case: startDayHour = 0 + { + startDayHour: 0, + endDayHour: 12, + offset: 0, + hours: [0, 6, 13], + }, + // Edge case: endDayHour = 24 + { + startDayHour: 12, + endDayHour: 24, + offset: 0, + hours: [11, 18, 23], + }, + ].forEach(({ + startDayHour, + endDayHour, + offset, + hours, + }) => { + hours.forEach((hour) => { + const testName = `startDayHour: ${startDayHour}, ` + + `endDayHour: ${endDayHour}, offset: ${offset}, ` + + `hour: ${hour}`; + + it(testName, async () => { + const { scheduler } = await createScheduler({ + views: ['timelineWeek'], + currentView: 'timelineWeek', + currentDate: new Date(2021, 1, 2), + startDayHour, + endDayHour, + offset, + }); + + const workspace = scheduler.getWorkSpace(); + const scrollable = workspace.getScrollable(); + const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); + const targetDate = new Date(2021, 1, 2, hour, 0); + + scheduler.scrollTo(targetDate, undefined, false); + const cell = workspace.viewDataProvider.findGlobalCellPosition( + targetDate, + 0, + false, + true, + ); + const cellStartDate = cell.cellData.startDate; + const cellEndDate = cell.cellData.endDate; + + expect(scrollBySpy).toHaveBeenCalledTimes(1); + expect(targetDate.getTime()).toBeGreaterThanOrEqual(cellStartDate.getTime()); + expect(targetDate.getTime()).toBeLessThan(cellEndDate.getTime()); + + scrollBySpy.mockRestore(); + }); }); - - const workspace = scheduler.getWorkSpace(); - const scrollable = workspace.getScrollable(); - const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); - - // In gap [6, 18), should NOT normalize to 18:00 Feb 2 (?) - const dateInGap = new Date(2021, 1, 2, 10, 0); - scheduler.scrollTo(dateInGap, undefined, false); - expect(scrollBySpy).not.toHaveBeenCalled(); - - scrollBySpy.mockClear(); - // In range [18, 24) on Feb 2, should scroll normally - const dateInFirstRange = new Date(2021, 1, 2, 22, 0); - scheduler.scrollTo(dateInFirstRange, undefined, false); - expect(scrollBySpy).toHaveBeenCalled(); - - scrollBySpy.mockClear(); - // In range [0, 6) but on wrong day (Feb 2), should NOT normalize to 18:00 Feb 2 (?) - const dateInSecondRangeWrongDay = new Date(2021, 1, 2, 3, 0); - scheduler.scrollTo(dateInSecondRangeWrongDay, undefined, false); - expect(scrollBySpy).not.toHaveBeenCalled(); - - scrollBySpy.mockClear(); - // In range [0, 6) on correct day (Feb 3), should scroll normally - const dateInSecondRangeCorrectDay = new Date(2021, 1, 3, 3, 0); - scheduler.scrollTo(dateInSecondRangeCorrectDay, undefined, false); - expect(scrollBySpy).toHaveBeenCalled(); - - scrollBySpy.mockRestore(); }); }); }); From d8b7b25de23d7cb24427e0a503b60c9b19d873eb Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Tue, 13 Jan 2026 12:00:52 +0100 Subject: [PATCH 09/13] test: add testcafe test --- .../tests/scheduler/common/scrollTo.ts | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts b/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts index 03be9d4df440..a15f6d32552f 100644 --- a/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts +++ b/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts @@ -32,6 +32,13 @@ const scrollToAllDay = ClientFunction(() => { instance.scrollTo(date, undefined, true); }); +const getWSScrollLeft = ClientFunction(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + return instance.getWorkSpaceScrollable().scrollLeft(); +}); + +const getHeaderScrollLeft = ClientFunction(() => $('.dx-scheduler-header-scrollable .dx-scrollable-container').scrollLeft()); + test('ScrollTo works correctly with week and day views', async (t) => { const scheduler = new Scheduler('#container'); @@ -230,3 +237,200 @@ test('ScrollTo works correctly in timeline RTL (native, sync header/workspace)', height: 580, rtlEnabled: true, })); + +test('T1310544: ScrollTo should scroll to date with offset: 720 (12 hours)', async (t) => { + const scheduler = new Scheduler('#container'); + + await scheduler.option('currentView', 'timelineDay'); + await scheduler.option('useNative', true); + await t.wait(200); + + const getExpectedScrollLeft = ClientFunction(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + const workspace = instance.getWorkSpace(); + const scrollable = workspace.getScrollable(); + const $scrollable = scrollable.$element(); + const scrollableWidth = $scrollable.width(); + + const targetDateValue = new Date(2021, 1, 2, 22, 0); + + const cell = workspace.viewDataProvider.findGlobalCellPosition( + targetDateValue, + 0, + false, + true, + ); + const { startDate: cellStartDate, endDate: cellEndDate } = cell.cellData; + + const isDateValid = targetDateValue.getTime() >= cellStartDate.getTime() + && targetDateValue.getTime() < cellEndDate.getTime(); + + // eslint-disable-next-line no-underscore-dangle + const cellCoordinates = workspace._getScrollCoordinates(targetDateValue, 0, false); + const cellWidth = workspace.getCellWidth(); + const expectedScrollLeft = cellCoordinates.left - (scrollableWidth - cellWidth) / 2; + + return { + expectedScrollLeft, + isDateValid, + cellStartDate: cellStartDate.getTime(), + cellEndDate: cellEndDate.getTime(), + targetDate: targetDateValue.getTime(), + }; + }); + + const initialLeft = await getWSScrollLeft(); + const initialHeaderLeft = await getHeaderScrollLeft(); + + await t.expect(initialLeft).eql(initialHeaderLeft, 'Initial header/workspace sync'); + + const cellData = await getExpectedScrollLeft(); + + await t + .expect(cellData.isDateValid) + .ok('Target date should be within cell range'); + + const scrollToTargetDate = ClientFunction((targetDate: Date) => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(targetDate, undefined, false); + }); + + await scrollToTargetDate(new Date(2021, 1, 2, 22, 0)); + await t.wait(300); + + const actualLeft = await getWSScrollLeft(); + const headerLeft = await getHeaderScrollLeft(); + + await t + .expect(actualLeft).notEql(initialLeft, 'Workspace should be scrolled') + .expect(headerLeft).eql(actualLeft, 'Header should be synchronized with workspace') + .expect(actualLeft) + .eql(cellData.expectedScrollLeft, 'Scroll position should match expected'); +}).before(async () => createScheduler({ + dataSource: [], + views: ['timelineDay'], + currentView: 'timelineDay', + currentDate: new Date(2021, 1, 2), + firstDayOfWeek: 0, + startDayHour: 6, + endDayHour: 18, + offset: 720, + cellDuration: 60, + height: 580, +})); + +[ + { + startDayHour: 6, + endDayHour: 18, + offset: 0, + hours: [4, 12, 20], + }, + { + startDayHour: 6, + endDayHour: 18, + offset: 360, + hours: [10, 15, 22], + }, + { + startDayHour: 6, + endDayHour: 18, + offset: -120, + hours: [3, 10, 20], + }, + { + startDayHour: 6, + endDayHour: 18, + offset: 720, + hours: [10, 22, 3], + }, + { + startDayHour: 0, + endDayHour: 12, + offset: 0, + hours: [0, 6, 13], + }, + { + startDayHour: 12, + endDayHour: 24, + offset: 0, + hours: [11, 18, 23], + }, +].forEach(({ + startDayHour, + endDayHour, + offset, + hours, +}) => { + hours.forEach((hour) => { + const getExpectedScrollPosition = ClientFunction((targetHour: number) => { + const instance = ($('#container') as any).dxScheduler('instance'); + const workspace = instance.getWorkSpace(); + const scrollable = workspace.getScrollable(); + const $scrollable = scrollable.$element(); + const scrollableWidth = $scrollable.width(); + const targetDate = new Date(2021, 1, 2, targetHour, 0); + + // eslint-disable-next-line no-underscore-dangle + const isValidDate = workspace._isValidScrollDate(targetDate, false); + + // eslint-disable-next-line no-underscore-dangle + const cellCoordinates = workspace._getScrollCoordinates(targetDate, 0, false); + const cellWidth = workspace.getCellWidth(); + const expectedScrollLeft = cellCoordinates.left - (scrollableWidth - cellWidth) / 2; + + return { + expectedScrollLeft, + isValidDate, + }; + }); + + const scrollToHour = ClientFunction((targetHour: number) => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(new Date(2021, 1, 2, targetHour, 0), undefined, false); + }); + + test(`Hour normalization: startDayHour: ${startDayHour}, endDayHour: ${endDayHour}, offset: ${offset}, hour: ${hour}`, async (t) => { + const scheduler = new Scheduler('#container'); + + await scheduler.option('currentView', 'timelineWeek'); + await scheduler.option('useNative', true); + await t.wait(200); + + const initialLeft = await getWSScrollLeft(); + const initialHeaderLeft = await getHeaderScrollLeft(); + + await t.expect(initialLeft).eql(initialHeaderLeft, 'Initial header/workspace sync'); + + const { expectedScrollLeft, isValidDate } = await getExpectedScrollPosition(hour); + + await t + .expect(isValidDate) + .ok(`Target date (hour: ${hour}) should be within valid scroll range`); + + await scrollToHour(hour); + await t.wait(300); + + const actualLeft = await getWSScrollLeft(); + const headerLeft = await getHeaderScrollLeft(); + + await t + .expect(actualLeft).notEql(initialLeft, 'Workspace should be scrolled') + .expect(headerLeft).eql(actualLeft, 'Header should be synchronized with workspace') + .expect(actualLeft) + .eql(expectedScrollLeft, 'Scroll position should match expected'); + }).before(async () => createScheduler({ + dataSource: [], + views: [{ + type: 'timelineWeek', + offset, + cellDuration: 60, + }], + currentView: 'timelineWeek', + currentDate: new Date(2021, 1, 2), + startDayHour, + endDayHour, + height: 580, + })); + }); +}); From 773235a3c8b1c4add8c352ccb14639c2f8ce31b0 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Wed, 14 Jan 2026 12:58:04 +0800 Subject: [PATCH 10/13] rewrite test --- .../tests/scheduler/common/scrollTo.ts | 244 ++++++------------ packages/testcafe-models/scheduler/index.ts | 36 ++- 2 files changed, 106 insertions(+), 174 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts b/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts index a15f6d32552f..ecd969aa55f2 100644 --- a/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts +++ b/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts @@ -32,13 +32,6 @@ const scrollToAllDay = ClientFunction(() => { instance.scrollTo(date, undefined, true); }); -const getWSScrollLeft = ClientFunction(() => { - const instance = ($('#container') as any).dxScheduler('instance'); - return instance.getWorkSpaceScrollable().scrollLeft(); -}); - -const getHeaderScrollLeft = ClientFunction(() => $('.dx-scheduler-header-scrollable .dx-scrollable-container').scrollLeft()); - test('ScrollTo works correctly with week and day views', async (t) => { const scheduler = new Scheduler('#container'); @@ -241,71 +234,14 @@ test('ScrollTo works correctly in timeline RTL (native, sync header/workspace)', test('T1310544: ScrollTo should scroll to date with offset: 720 (12 hours)', async (t) => { const scheduler = new Scheduler('#container'); - await scheduler.option('currentView', 'timelineDay'); - await scheduler.option('useNative', true); - await t.wait(200); - - const getExpectedScrollLeft = ClientFunction(() => { - const instance = ($('#container') as any).dxScheduler('instance'); - const workspace = instance.getWorkSpace(); - const scrollable = workspace.getScrollable(); - const $scrollable = scrollable.$element(); - const scrollableWidth = $scrollable.width(); - - const targetDateValue = new Date(2021, 1, 2, 22, 0); - - const cell = workspace.viewDataProvider.findGlobalCellPosition( - targetDateValue, - 0, - false, - true, - ); - const { startDate: cellStartDate, endDate: cellEndDate } = cell.cellData; - - const isDateValid = targetDateValue.getTime() >= cellStartDate.getTime() - && targetDateValue.getTime() < cellEndDate.getTime(); - - // eslint-disable-next-line no-underscore-dangle - const cellCoordinates = workspace._getScrollCoordinates(targetDateValue, 0, false); - const cellWidth = workspace.getCellWidth(); - const expectedScrollLeft = cellCoordinates.left - (scrollableWidth - cellWidth) / 2; - - return { - expectedScrollLeft, - isDateValid, - cellStartDate: cellStartDate.getTime(), - cellEndDate: cellEndDate.getTime(), - targetDate: targetDateValue.getTime(), - }; - }); - - const initialLeft = await getWSScrollLeft(); - const initialHeaderLeft = await getHeaderScrollLeft(); - - await t.expect(initialLeft).eql(initialHeaderLeft, 'Initial header/workspace sync'); + const targetDate = new Date(2021, 1, 2, 22, 0); + await scheduler.scrollTo(targetDate); - const cellData = await getExpectedScrollLeft(); + const cellData = await scheduler.getCellDataAtViewportCenter(); await t - .expect(cellData.isDateValid) - .ok('Target date should be within cell range'); - - const scrollToTargetDate = ClientFunction((targetDate: Date) => { - const instance = ($('#container') as any).dxScheduler('instance'); - instance.scrollTo(targetDate, undefined, false); - }); - - await scrollToTargetDate(new Date(2021, 1, 2, 22, 0)); - await t.wait(300); - - const actualLeft = await getWSScrollLeft(); - const headerLeft = await getHeaderScrollLeft(); - - await t - .expect(actualLeft).notEql(initialLeft, 'Workspace should be scrolled') - .expect(headerLeft).eql(actualLeft, 'Header should be synchronized with workspace') - .expect(actualLeft) - .eql(cellData.expectedScrollLeft, 'Scroll position should match expected'); + .expect(targetDate.getTime()).gte(cellData.startDate.getTime()) + .expect(targetDate.getTime()).lt(cellData.endDate.getTime()); }).before(async () => createScheduler({ dataSource: [], views: ['timelineDay'], @@ -320,117 +256,89 @@ test('T1310544: ScrollTo should scroll to date with offset: 720 (12 hours)', asy })); [ + // startDayHour: 6:00, endDayHour: 18:00 { - startDayHour: 6, - endDayHour: 18, offset: 0, - hours: [4, 12, 20], + targetDate: new Date(2021, 1, 3, 4, 0), + expectedDate: new Date(2021, 1, 3, 6, 0), }, { - startDayHour: 6, - endDayHour: 18, - offset: 360, - hours: [10, 15, 22], + offset: 0, + targetDate: new Date(2021, 1, 3, 12, 0), + expectedDate: new Date(2021, 1, 3, 12, 0), }, { - startDayHour: 6, - endDayHour: 18, - offset: -120, - hours: [3, 10, 20], + offset: 0, + targetDate: new Date(2021, 1, 3, 20, 0), + expectedDate: new Date(2021, 1, 3, 18, 0), }, + + // startDayHour: 18:00, endDayHour: next day 6:00 { - startDayHour: 6, - endDayHour: 18, offset: 720, - hours: [10, 22, 3], + targetDate: new Date(2021, 1, 3, 10, 0), + expectedDate: new Date(2021, 1, 3, 6, 0), }, { - startDayHour: 0, - endDayHour: 12, - offset: 0, - hours: [0, 6, 13], + offset: 720, + targetDate: new Date(2021, 1, 3, 20, 0), + expectedDate: new Date(2021, 1, 3, 20, 0), }, { - startDayHour: 12, - endDayHour: 24, - offset: 0, - hours: [11, 18, 23], + offset: 720, + targetDate: new Date(2021, 1, 4, 1, 0), + expectedDate: new Date(2021, 1, 4, 1, 0), + }, + { + offset: 720, + targetDate: new Date(2021, 1, 4, 7, 0), + expectedDate: new Date(2021, 1, 4, 6, 0), + }, + + // startDayHour: prev day 18:00, endDayHour: 6:00 + { + offset: -720, + targetDate: new Date(2021, 1, 3, 16, 0), + expectedDate: new Date(2021, 1, 3, 18, 0), + }, + { + offset: -720, + targetDate: new Date(2021, 1, 3, 21, 0), + expectedDate: new Date(2021, 1, 3, 21, 0), + }, + { + offset: -720, + targetDate: new Date(2021, 1, 4, 3, 0), + expectedDate: new Date(2021, 1, 4, 3, 0), + }, + { + offset: -720, + targetDate: new Date(2021, 1, 3, 7, 0), + expectedDate: new Date(2021, 1, 3, 6, 0), }, -].forEach(({ - startDayHour, - endDayHour, - offset, - hours, -}) => { - hours.forEach((hour) => { - const getExpectedScrollPosition = ClientFunction((targetHour: number) => { - const instance = ($('#container') as any).dxScheduler('instance'); - const workspace = instance.getWorkSpace(); - const scrollable = workspace.getScrollable(); - const $scrollable = scrollable.$element(); - const scrollableWidth = $scrollable.width(); - const targetDate = new Date(2021, 1, 2, targetHour, 0); - - // eslint-disable-next-line no-underscore-dangle - const isValidDate = workspace._isValidScrollDate(targetDate, false); - - // eslint-disable-next-line no-underscore-dangle - const cellCoordinates = workspace._getScrollCoordinates(targetDate, 0, false); - const cellWidth = workspace.getCellWidth(); - const expectedScrollLeft = cellCoordinates.left - (scrollableWidth - cellWidth) / 2; - - return { - expectedScrollLeft, - isValidDate, - }; - }); - - const scrollToHour = ClientFunction((targetHour: number) => { - const instance = ($('#container') as any).dxScheduler('instance'); - instance.scrollTo(new Date(2021, 1, 2, targetHour, 0), undefined, false); - }); - - test(`Hour normalization: startDayHour: ${startDayHour}, endDayHour: ${endDayHour}, offset: ${offset}, hour: ${hour}`, async (t) => { - const scheduler = new Scheduler('#container'); - - await scheduler.option('currentView', 'timelineWeek'); - await scheduler.option('useNative', true); - await t.wait(200); - - const initialLeft = await getWSScrollLeft(); - const initialHeaderLeft = await getHeaderScrollLeft(); - - await t.expect(initialLeft).eql(initialHeaderLeft, 'Initial header/workspace sync'); - - const { expectedScrollLeft, isValidDate } = await getExpectedScrollPosition(hour); - - await t - .expect(isValidDate) - .ok(`Target date (hour: ${hour}) should be within valid scroll range`); - - await scrollToHour(hour); - await t.wait(300); - - const actualLeft = await getWSScrollLeft(); - const headerLeft = await getHeaderScrollLeft(); - - await t - .expect(actualLeft).notEql(initialLeft, 'Workspace should be scrolled') - .expect(headerLeft).eql(actualLeft, 'Header should be synchronized with workspace') - .expect(actualLeft) - .eql(expectedScrollLeft, 'Scroll position should match expected'); - }).before(async () => createScheduler({ - dataSource: [], - views: [{ - type: 'timelineWeek', - offset, - cellDuration: 60, - }], - currentView: 'timelineWeek', - currentDate: new Date(2021, 1, 2), - startDayHour, - endDayHour, - height: 580, - })); - }); +].forEach(({ offset, targetDate, expectedDate }) => { + test(`Hour normalization: offset=${offset}, targetDate=${targetDate.toString()}`, async (t) => { + const scheduler = new Scheduler('#container'); + + await scheduler.scrollTo(targetDate); + + const cellData = await scheduler.getCellDataAtViewportCenter(); + + await t + .expect(expectedDate.getTime()).gte(cellData.startDate.getTime()) + // eslint-disable-next-line spellcheck/spell-checker + .expect(expectedDate.getTime()).lte(cellData.endDate.getTime()); + }).before(async () => createScheduler({ + dataSource: [], + views: [{ + type: 'timelineWeek', + offset, + cellDuration: 60, + }], + currentView: 'timelineWeek', + currentDate: new Date(2021, 1, 2), + startDayHour: 6, + endDayHour: 18, + height: 580, + })); }); diff --git a/packages/testcafe-models/scheduler/index.ts b/packages/testcafe-models/scheduler/index.ts index 5c28a5bd04d0..656ba4d115e2 100644 --- a/packages/testcafe-models/scheduler/index.ts +++ b/packages/testcafe-models/scheduler/index.ts @@ -163,6 +163,28 @@ export default class Scheduler extends Widget { return cells.filter(`.${CLASS.focusedCell}`); } + getCellDataAtViewportCenter(): any { + const { getInstance } = this; + + return ClientFunction( + () => { + const instance = getInstance() as any; + const workSpace = instance.getWorkSpace(); + const scrollable = workSpace.getScrollable(); + const scrollLeft = scrollable.scrollLeft(); + const scrollTop = scrollable.scrollTop(); + const centerX = scrollLeft + scrollable.$element().width() / 2; + const centerY = scrollTop + scrollable.$element().height() / 2; + + const cellElement = workSpace.getCellByCoordinates({ top: centerY, left: centerX }, false); + const cellData = workSpace.getCellData(cellElement); + + return cellData; + }, + { dependencies: { getInstance } } + )(); + } + getSelectedCells(isAllDay = false): Selector { const cells = isAllDay ? this.allDayTableCells : this.dateTableCells; @@ -197,13 +219,15 @@ export default class Scheduler extends Widget { scrollTo(date: Date, group?: Record, allDay?: boolean): Promise { const { getInstance } = this; - const scrollTo = (): any => (getInstance() as any).scrollTo(date, group, allDay); - return ClientFunction(scrollTo, { - dependencies: { - date, group, allDay, getInstance, - }, - })(); + return ClientFunction( + () => { + const instance = getInstance() as any; + instance.scrollTo(date, group, allDay); + }, { + dependencies: { date, group, allDay, getInstance }, + } + )(); } hideAppointmentTooltip(): Promise { From 62694e619a0348448a38a5a4145987fd2c50e78e Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Wed, 14 Jan 2026 13:00:42 +0800 Subject: [PATCH 11/13] revert workspace jest test changes --- .../__tests__/workspace.recalculation.test.ts | 79 +++++++ .../scheduler/__tests__/workspace.test.ts | 219 ------------------ 2 files changed, 79 insertions(+), 219 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/__tests__/workspace.recalculation.test.ts delete mode 100644 packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.recalculation.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.recalculation.test.ts new file mode 100644 index 000000000000..13ffa2259716 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.recalculation.test.ts @@ -0,0 +1,79 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import $ from '@js/core/renderer'; + +import fx from '../../../common/core/animation/fx'; +import CustomStore from '../../../data/custom_store'; +import { createScheduler } from './__mock__/create_scheduler'; +import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; + +const CLASSES = { + scheduler: 'dx-scheduler', + workSpace: 'dx-scheduler-work-space', +}; + +describe('Workspace Recalculation with Async Templates (T661335)', () => { + beforeEach(() => { + fx.off = true; + setupSchedulerTestEnvironment({ height: 600 }); + }); + + afterEach(() => { + const $scheduler = $(document.querySelector(`.${CLASSES.scheduler}`)); + // @ts-expect-error + $scheduler.dxScheduler('dispose'); + document.body.innerHTML = ''; + fx.off = false; + }); + + it('should not duplicate workspace elements when resources are loaded asynchronously (T661335)', async () => { + const { scheduler, container } = await createScheduler({ + templatesRenderAsynchronously: true, + currentView: 'day', + views: ['day'], + groups: ['owner'], + resources: [ + { + fieldExpr: 'owner', + dataSource: [{ id: 1, text: 'Owner 1' }], + }, + { + fieldExpr: 'room', + dataSource: new CustomStore({ + load(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve([{ id: 1, text: 'Room 1', color: '#ff0000' }]); + }); + }); + }, + }), + }, + ], + dataSource: [ + { + text: 'Meeting in Room 1', + startDate: new Date(2017, 4, 25, 9, 0), + endDate: new Date(2017, 4, 25, 10, 0), + roomId: 1, + }, + ], + startDayHour: 9, + currentDate: new Date(2017, 4, 25), + height: 600, + }); + + scheduler.option('groups', ['room']); + + await new Promise((r) => { setTimeout(r); }); + + const $workSpaces = $(container).find(`.${CLASSES.workSpace}`); + const $groupHeader = $(container).find('.dx-scheduler-group-header'); + + expect($workSpaces.length).toBe(1); + + expect($groupHeader.length).toBeGreaterThan(0); + expect($groupHeader.text()).toContain('Room 1'); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts deleted file mode 100644 index d4fef5e04e75..000000000000 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { - afterEach, beforeEach, describe, expect, it, jest, -} from '@jest/globals'; -import $ from '@js/core/renderer'; -import { getWidth } from '@js/core/utils/size'; - -import fx from '../../../common/core/animation/fx'; -import CustomStore from '../../../data/custom_store'; -import { createScheduler } from './__mock__/create_scheduler'; -import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; - -const CLASSES = { - scheduler: 'dx-scheduler', - workSpace: 'dx-scheduler-work-space', -}; - -describe('Workspace', () => { - describe('Recalculation with Async Templates (T661335)', () => { - beforeEach(() => { - fx.off = true; - setupSchedulerTestEnvironment({ height: 600 }); - }); - - afterEach(() => { - const $scheduler = $(document.querySelector(`.${CLASSES.scheduler}`)); - // @ts-expect-error - $scheduler.dxScheduler('dispose'); - document.body.innerHTML = ''; - fx.off = false; - }); - - it('should not duplicate workspace elements when resources are loaded asynchronously (T661335)', async () => { - const { scheduler, container } = await createScheduler({ - templatesRenderAsynchronously: true, - currentView: 'day', - views: ['day'], - groups: ['owner'], - resources: [ - { - fieldExpr: 'owner', - dataSource: [{ id: 1, text: 'Owner 1' }], - }, - { - fieldExpr: 'room', - dataSource: new CustomStore({ - load(): Promise { - return new Promise((resolve) => { - setTimeout(() => { - resolve([{ id: 1, text: 'Room 1', color: '#ff0000' }]); - }); - }); - }, - }), - }, - ], - dataSource: [ - { - text: 'Meeting in Room 1', - startDate: new Date(2017, 4, 25, 9, 0), - endDate: new Date(2017, 4, 25, 10, 0), - roomId: 1, - }, - ], - startDayHour: 9, - currentDate: new Date(2017, 4, 25), - height: 600, - }); - - scheduler.option('groups', ['room']); - - await new Promise((r) => { setTimeout(r); }); - - const $workSpaces = $(container).find(`.${CLASSES.workSpace}`); - const $groupHeader = $(container).find('.dx-scheduler-group-header'); - - expect($workSpaces.length).toBe(1); - - expect($groupHeader.length).toBeGreaterThan(0); - expect($groupHeader.text()).toContain('Room 1'); - }); - }); - - describe('scrollTo', () => { - beforeEach(() => { - fx.off = true; - setupSchedulerTestEnvironment({ height: 600 }); - }); - - afterEach(() => { - document.body.innerHTML = ''; - fx.off = false; - }); - - it('T1310544: should scroll to date with offset: 720 (12 hours)', async () => { - const { scheduler } = await createScheduler({ - views: ['timelineDay'], - currentView: 'timelineDay', - currentDate: new Date(2021, 1, 2), - firstDayOfWeek: 0, - startDayHour: 6, - endDayHour: 18, - offset: 720, - cellDuration: 60, - height: 580, - }); - - const workspace = scheduler.getWorkSpace(); - const scrollable = workspace.getScrollable(); - const $scrollable = scrollable.$element(); - const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); - - // With offset: 720 (12 hours), cells start at 18:00 (6:00 + 12h) - // For date 22:00, this should be cell index 4 (18:00=0, 19:00=1, 20:00=2, 21:00=3, 22:00=4) - const leftCellCount = 4; - const cellWidth = workspace.getCellWidth(); - const scrollableWidth = getWidth($scrollable); - const expectedLeft = leftCellCount * cellWidth - (scrollableWidth - cellWidth) / 2; - - const targetDate = new Date(2021, 1, 2, 22, 0); - scheduler.scrollTo(targetDate, undefined, false); - - expect(scrollBySpy).toHaveBeenCalledTimes(1); - const scrollParams = scrollBySpy.mock.calls[0][0] as { left: number; top: number }; - expect(scrollParams.left).toBeCloseTo(expectedLeft, 1); - - scrollBySpy.mockRestore(); - }); - - describe('hour normalization', () => { - [ - // Without offset, normal range - { - startDayHour: 6, - endDayHour: 18, - offset: 0, - hours: [4, 12, 20], - }, - // With positive offset - { - startDayHour: 6, - endDayHour: 18, - offset: 360, - hours: [10, 15, 22], - }, - // With negative offset - { - startDayHour: 6, - endDayHour: 18, - offset: -120, - hours: [3, 10, 20], - }, - // With offset creating midnight crossing - { - startDayHour: 6, - endDayHour: 18, - offset: 720, - hours: [10, 22, 3], - }, - // Edge case: startDayHour = 0 - { - startDayHour: 0, - endDayHour: 12, - offset: 0, - hours: [0, 6, 13], - }, - // Edge case: endDayHour = 24 - { - startDayHour: 12, - endDayHour: 24, - offset: 0, - hours: [11, 18, 23], - }, - ].forEach(({ - startDayHour, - endDayHour, - offset, - hours, - }) => { - hours.forEach((hour) => { - const testName = `startDayHour: ${startDayHour}, ` - + `endDayHour: ${endDayHour}, offset: ${offset}, ` - + `hour: ${hour}`; - - it(testName, async () => { - const { scheduler } = await createScheduler({ - views: ['timelineWeek'], - currentView: 'timelineWeek', - currentDate: new Date(2021, 1, 2), - startDayHour, - endDayHour, - offset, - }); - - const workspace = scheduler.getWorkSpace(); - const scrollable = workspace.getScrollable(); - const scrollBySpy = jest.spyOn(scrollable, 'scrollBy'); - const targetDate = new Date(2021, 1, 2, hour, 0); - - scheduler.scrollTo(targetDate, undefined, false); - const cell = workspace.viewDataProvider.findGlobalCellPosition( - targetDate, - 0, - false, - true, - ); - const cellStartDate = cell.cellData.startDate; - const cellEndDate = cell.cellData.endDate; - - expect(scrollBySpy).toHaveBeenCalledTimes(1); - expect(targetDate.getTime()).toBeGreaterThanOrEqual(cellStartDate.getTime()); - expect(targetDate.getTime()).toBeLessThan(cellEndDate.getTime()); - - scrollBySpy.mockRestore(); - }); - }); - }); - }); - }); -}); From 0a62a20193f4b605e3c3f5e65ff6baf1c60b8554 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Wed, 14 Jan 2026 13:20:31 +0800 Subject: [PATCH 12/13] remove duplicate test --- .../tests/scheduler/common/scrollTo.ts | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts b/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts index ecd969aa55f2..e405b63bcacc 100644 --- a/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts +++ b/e2e/testcafe-devextreme/tests/scheduler/common/scrollTo.ts @@ -231,30 +231,6 @@ test('ScrollTo works correctly in timeline RTL (native, sync header/workspace)', rtlEnabled: true, })); -test('T1310544: ScrollTo should scroll to date with offset: 720 (12 hours)', async (t) => { - const scheduler = new Scheduler('#container'); - - const targetDate = new Date(2021, 1, 2, 22, 0); - await scheduler.scrollTo(targetDate); - - const cellData = await scheduler.getCellDataAtViewportCenter(); - - await t - .expect(targetDate.getTime()).gte(cellData.startDate.getTime()) - .expect(targetDate.getTime()).lt(cellData.endDate.getTime()); -}).before(async () => createScheduler({ - dataSource: [], - views: ['timelineDay'], - currentView: 'timelineDay', - currentDate: new Date(2021, 1, 2), - firstDayOfWeek: 0, - startDayHour: 6, - endDayHour: 18, - offset: 720, - cellDuration: 60, - height: 580, -})); - [ // startDayHour: 6:00, endDayHour: 18:00 { @@ -317,7 +293,7 @@ test('T1310544: ScrollTo should scroll to date with offset: 720 (12 hours)', asy expectedDate: new Date(2021, 1, 3, 6, 0), }, ].forEach(({ offset, targetDate, expectedDate }) => { - test(`Hour normalization: offset=${offset}, targetDate=${targetDate.toString()}`, async (t) => { + test(`scrollTo should scroll to date with offset=${offset}, targetDate=${targetDate.toString()} (T1310544)`, async (t) => { const scheduler = new Scheduler('#container'); await scheduler.scrollTo(targetDate); From 9afeb16889dff8be5d812f1bb619762c6247f696 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Wed, 14 Jan 2026 15:36:31 +0800 Subject: [PATCH 13/13] apply copilot's review --- .../scheduler/workspaces/m_work_space.ts | 14 +++++++++++--- .../workspaces/view_model/m_view_data_provider.ts | 8 ++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index 40a255e824ac..f8d158c7fbad 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -1412,11 +1412,15 @@ class SchedulerWorkSpace extends Widget { const cell = this.viewDataProvider.findGlobalCellPosition(currentDate, groupIndex, allDay, true); - currentDate.setHours(cell?.cellData.startDate.getHours(), currentDate.getMinutes(), 0, 0); + if (!cell) { + return undefined; + } + + currentDate.setHours(cell.cellData.startDate.getHours(), currentDate.getMinutes(), 0, 0); return this.virtualScrollingDispatcher.calculateCoordinatesByDataAndPosition( - cell?.cellData, - cell?.position, + cell.cellData, + cell.position, currentDate, isDateAndTimeView(this.type as any), this.viewDirection === 'vertical', @@ -1825,6 +1829,10 @@ class SchedulerWorkSpace extends Widget { const coordinates = this._getScrollCoordinates(date, groupIndex, isScrollToAllDay); + if (!coordinates) { + return; + } + const scrollable = this.getScrollable(); const $scrollable = scrollable.$element(); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts index 3cf33b098191..ac1c485234a6 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts @@ -292,11 +292,11 @@ export default class ViewDataProvider { let resultCellColumnIndex = -1; let resultCellRowIndex = -1; - const getCellPosition = (rowIndex: number, cellIndex: number) => ({ - columnIndex: rowIndex, + const getCellPosition = (columnIndex: number, rowIndex: number) => ({ + columnIndex, rowIndex: showAllDayPanel && !this._options.isVerticalGrouping - ? cellIndex - 1 - : cellIndex, + ? rowIndex - 1 + : rowIndex, }); for (let rowIndex = 0; rowIndex < completeViewDataMap.length; rowIndex += 1) {