Skip to content

Commit ef9793e

Browse files
committed
add jest test
1 parent 08e71b6 commit ef9793e

File tree

2 files changed

+178
-1
lines changed

2 files changed

+178
-1
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/* eslint-disable @typescript-eslint/no-unsafe-return */
3+
/* eslint-disable @typescript-eslint/init-declarations */
4+
/* eslint-disable @typescript-eslint/ban-ts-comment */
5+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
6+
7+
import {
8+
afterEach, beforeEach, describe, expect, jest, test,
9+
} from '@jest/globals';
10+
import { Deferred } from '@js/core/utils/deferred';
11+
import type { Store } from '@js/data';
12+
import CustomStore from '@js/data/custom_store';
13+
import DataSource from '@js/data/data_source';
14+
15+
import { DataSourceAdapterTreeList } from './m_data_source_adapter';
16+
17+
describe('TreeList DataSourceAdapter - T1311885 Race Condition', () => {
18+
let dataSourceAdapter: DataSourceAdapterTreeList;
19+
let mockStore: Store;
20+
let loadCalls: { filter: any; deferred: any; type?: string }[];
21+
const parentData = [
22+
{ Task_ID: 1, Task_Parent_ID: 0, Task_Subject: 'Parent 1' },
23+
{ Task_ID: 2, Task_Parent_ID: 0, Task_Subject: 'Parent 2' },
24+
];
25+
26+
const childData = [
27+
{ Task_ID: 10, Task_Parent_ID: 1, Task_Subject: 'Child 1' },
28+
{ Task_ID: 20, Task_Parent_ID: 2, Task_Subject: 'Child 2' },
29+
];
30+
31+
const OPERATION_ID = {
32+
FIRST: 1,
33+
SECOND: 2,
34+
};
35+
36+
beforeEach(() => {
37+
loadCalls = [];
38+
39+
mockStore = new CustomStore({
40+
key: 'Task_ID',
41+
load: (options: any) => {
42+
// @ts-expect-error
43+
const deferred = new Deferred();
44+
loadCalls.push({ filter: options?.filter, deferred });
45+
return deferred.promise();
46+
},
47+
// byKey: jest.fn((key: any) => {
48+
// // @ts-expect-error
49+
// const deferred = new Deferred();
50+
// const item = parentData.find((p) => p.Task_ID === key);
51+
// deferred.resolve(item);
52+
// return deferred.promise();
53+
// }),
54+
});
55+
56+
const dataSource = new DataSource({
57+
store: mockStore,
58+
reshapeOnPush: true,
59+
});
60+
61+
const mockComponent = {
62+
option: jest.fn((key: string) => {
63+
const options: any = {
64+
remoteOperations: { filtering: true, sorting: true },
65+
parentIdExpr: 'Task_Parent_ID',
66+
hasItemsExpr: 'Has_Items',
67+
filterMode: 'fullBranch',
68+
expandedRowKeys: [],
69+
dataStructure: 'plain',
70+
rootValue: 0,
71+
};
72+
return options[key];
73+
}),
74+
_createActionByOption: jest.fn(() => jest.fn()),
75+
on: jest.fn(() => mockComponent),
76+
off: jest.fn(() => mockComponent),
77+
_eventsStrategy: {
78+
on: jest.fn(),
79+
off: jest.fn(),
80+
fireEvent: jest.fn(),
81+
hasEvent: jest.fn(() => false),
82+
},
83+
} as any;
84+
85+
dataSourceAdapter = new DataSourceAdapterTreeList(mockComponent);
86+
dataSourceAdapter.init(dataSource, { remoteOperations: { filtering: true } });
87+
88+
(dataSourceAdapter as any)._loadDataSource = jest.fn((options: any) => {
89+
// @ts-expect-error
90+
const deferred = new Deferred();
91+
92+
loadCalls.push({
93+
filter: options?.filter,
94+
deferred,
95+
type: 'dataSource',
96+
});
97+
98+
return deferred.promise();
99+
});
100+
});
101+
102+
afterEach(() => {
103+
// Cleanup mocks and spies
104+
jest.clearAllMocks();
105+
jest.restoreAllMocks();
106+
107+
// Clear references to prevent memory leaks
108+
loadCalls = [];
109+
(dataSourceAdapter as any)._loadDataSource = undefined;
110+
(dataSourceAdapter as any).loadFromStore = undefined;
111+
mockStore = undefined as any;
112+
dataSourceAdapter = undefined as any;
113+
});
114+
115+
test('T1311885 - _loadParentsOrChildren should NOT throw concat error when _cachedStoreData is cleared', async () => {
116+
let firstLoadDeferred: any = null;
117+
let errorMessage = '';
118+
119+
const unhandledRejectionHandler = (reason: any) => {
120+
errorMessage = reason?.message || String(reason);
121+
};
122+
process.on('unhandledRejection', unhandledRejectionHandler);
123+
124+
// Setup initial state
125+
(dataSourceAdapter as any)._cachedStoreData = parentData;
126+
(dataSourceAdapter as any)._dataSource = {
127+
store: jest.fn(() => mockStore),
128+
};
129+
(dataSourceAdapter as any)._lastOperationId = OPERATION_ID.FIRST;
130+
131+
// Mock options
132+
const options = {
133+
remoteOperations: { filtering: true },
134+
storeLoadOptions: { sort: null },
135+
loadOptions: { sort: null },
136+
operationId: OPERATION_ID.FIRST,
137+
};
138+
139+
// Spy on loadFromStore to capture and control the deferred
140+
141+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
142+
(dataSourceAdapter as any).loadFromStore = jest.fn((loadOptions, store) => {
143+
// @ts-expect-error
144+
const deferred = new Deferred();
145+
146+
// Save first deferred for later manipulation
147+
if (!firstLoadDeferred) {
148+
firstLoadDeferred = deferred;
149+
}
150+
151+
return deferred.promise();
152+
});
153+
154+
// STEP 1: Start first _loadParentsOrChildren call (parent lookup)
155+
(dataSourceAdapter as any)._loadParentsOrChildren(
156+
childData,
157+
options,
158+
);
159+
160+
// Verify loadFromStore was called
161+
expect((dataSourceAdapter as any).loadFromStore).toHaveBeenCalledTimes(1);
162+
expect(firstLoadDeferred).toBeDefined();
163+
164+
// STEP 2: Simulate reload() - clears _cachedStoreData
165+
(dataSourceAdapter as any)._cachedStoreData = undefined;
166+
(dataSourceAdapter as any)._lastOperationId = OPERATION_ID.SECOND;
167+
168+
// STEP 3: Resolve OLD parent lookup
169+
firstLoadDeferred.resolve(parentData);
170+
await Promise.resolve();
171+
172+
process.off('unhandledRejection', unhandledRejectionHandler);
173+
174+
expect(errorMessage).toBe('');
175+
expect(errorMessage).not.toMatch(/concat|Cannot read properties of undefined/i);
176+
});
177+
});

packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const applySorting = (data: any[], sort: any): any => queryByOptions(
4343
},
4444
).toArray();
4545

46-
class DataSourceAdapterTreeList extends DataSourceAdapter {
46+
export class DataSourceAdapterTreeList extends DataSourceAdapter {
4747
private _indexByKey: any;
4848

4949
private _keyGetter: any;

0 commit comments

Comments
 (0)