Skip to content

Commit 6c60262

Browse files
committed
feat: Implement recursive mergeFrom and refactor Portal Containers (#8571, #8572, #8573)
1 parent 038cf49 commit 6c60262

1 file changed

Lines changed: 127 additions & 2 deletions

File tree

test/playwright/unit/core/ConfigMerging.spec.mjs

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Neo from '../../../../src/Neo.mjs';
1111
import * as core from '../../../../src/core/_export.mjs';
1212
import Container from '../../../../src/container/Base.mjs';
1313
import Component from '../../../../src/component/Base.mjs';
14-
import Button from '../../../../src/button/Base.mjs'; // Required for ntype: button
14+
import Button from '../../../../src/button/Base.mjs';
1515
import {isDescriptor, mergeFrom} from '../../../../src/core/ConfigSymbols.mjs';
1616

1717
test.describe('Declarative Config Merging', () => {
@@ -85,4 +85,129 @@ test.describe('Declarative Config Merging', () => {
8585
expect(btn.text).toBe('Overridden Text');
8686
expect(btn.iconCls).toBe('home');
8787
});
88-
});
88+
89+
test('Prototype Pollution: Subclasses should not share state when clone: deep is used', () => {
90+
// 1. Define Shared Base
91+
class SharedBase extends Container {
92+
static config = {
93+
className: 'Neo.test.Pollution.Shared',
94+
95+
commonConfig_: {
96+
[isDescriptor]: true,
97+
merge : 'deep',
98+
value : null // To be overridden
99+
},
100+
101+
items: {
102+
[isDescriptor]: true,
103+
merge : 'deep',
104+
clone : 'deep', // CRITICAL for isolation
105+
value : {
106+
child: {
107+
ntype : 'component',
108+
[mergeFrom]: 'commonConfig'
109+
}
110+
}
111+
}
112+
}
113+
}
114+
SharedBase = Neo.setupClass(SharedBase);
115+
116+
// 2. Define Subclass A
117+
class ClassA extends SharedBase {
118+
static config = {
119+
className: 'Neo.test.Pollution.ClassA',
120+
commonConfig: {
121+
text: 'Text A'
122+
}
123+
}
124+
}
125+
ClassA = Neo.setupClass(ClassA);
126+
127+
// 3. Define Subclass B
128+
class ClassB extends SharedBase {
129+
static config = {
130+
className: 'Neo.test.Pollution.ClassB',
131+
commonConfig: {
132+
text: 'Text B'
133+
}
134+
}
135+
}
136+
ClassB = Neo.setupClass(ClassB);
137+
138+
// 4. Instantiate A
139+
const instanceA = Neo.create(ClassA);
140+
const childA = instanceA.items[0];
141+
expect(childA.text).toBe('Text A');
142+
143+
// 5. Instantiate B
144+
const instanceB = Neo.create(ClassB);
145+
const childB = instanceB.items[0];
146+
147+
// If pollution occurred, B might see 'Text A' or the mergeFrom symbol might be gone
148+
expect(childB.text).toBe('Text B');
149+
});
150+
151+
test('Nested Object Items Pollution Check', () => {
152+
// Verify recursion + isolation
153+
class NestedBase extends Container {
154+
static config = {
155+
className: 'Neo.test.Pollution.NestedBase',
156+
157+
targetConfig_: {
158+
[isDescriptor]: true,
159+
merge : 'deep',
160+
value : null
161+
},
162+
163+
items: {
164+
[isDescriptor]: true,
165+
merge : 'deep',
166+
clone : 'deep',
167+
value : {
168+
wrapper: {
169+
ntype: 'container',
170+
items: {
171+
target: {
172+
ntype : 'component',
173+
[mergeFrom]: 'targetConfig'
174+
}
175+
}
176+
}
177+
}
178+
}
179+
}
180+
}
181+
NestedBase = Neo.setupClass(NestedBase);
182+
183+
class NestedA extends NestedBase {
184+
static config = {
185+
className: 'Neo.test.Pollution.NestedA',
186+
targetConfig: { cls: ['class-a'] }
187+
}
188+
}
189+
NestedA = Neo.setupClass(NestedA);
190+
191+
class NestedB extends NestedBase {
192+
static config = {
193+
className: 'Neo.test.Pollution.NestedB',
194+
targetConfig: { cls: ['class-b'] }
195+
}
196+
}
197+
NestedB = Neo.setupClass(NestedB);
198+
199+
const a = Neo.create(NestedA);
200+
// Note: wrapper is a Container. We need to check ITS items.
201+
// But Container items are lazily created or array-ified.
202+
// Wrapper item in 'a' is an instance? No, 'wrapper' in 'a.items' is an instance.
203+
// 'wrapper' instance has 'items'.
204+
const wrapperA = a.items[0];
205+
const targetA = wrapperA.items[0];
206+
expect(targetA.cls).toEqual(['class-a']);
207+
208+
const b = Neo.create(NestedB);
209+
const wrapperB = b.items[0];
210+
const targetB = wrapperB.items[0];
211+
expect(targetB.cls).toEqual(['class-b']);
212+
});
213+
});

0 commit comments

Comments
 (0)