Skip to content

Commit 9926f04

Browse files
committed
feat: add testing harness
1 parent 59169c6 commit 9926f04

File tree

5 files changed

+241
-42
lines changed

5 files changed

+241
-42
lines changed

projects/ngx-json-treeview/src/lib/directives/stop-click-propagation.directive.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
import { Directive, HostListener, input } from '@angular/core';
1+
import { Directive, input } from '@angular/core';
22

33
@Directive({
44
selector: '[ngxJtStopClickPropagation]',
55
standalone: true,
6+
host: {
7+
'(click)': 'onClick($event)',
8+
},
69
})
710
export class StopClickPropagationDirective {
8-
enabled = input<boolean>(true, { alias: 'ngxJtStopClickPropagation' });
11+
readonly enabled = input<boolean>(true, {
12+
alias: 'ngxJtStopClickPropagation',
13+
});
914

10-
@HostListener('click', ['$event'])
11-
onClick(event: Event): void {
15+
protected onClick(event: Event): void {
1216
if (this.enabled()) {
1317
event.stopPropagation();
1418
}

projects/ngx-json-treeview/src/lib/ngx-json-treeview/ngx-json-treeview.component.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
22
import { provideZonelessChangeDetection } from '@angular/core';
33
import { TestBed } from '@angular/core/testing';
44
import { NgxJsonTreeviewComponent } from './ngx-json-treeview.component';
5+
import { NgxJsonTreeviewNodeHarness } from './testing/ngx-json-treeview.harness';
56

67
async function setupTest({
78
json = {},
9+
depth = 0,
810
}: {
11+
depth?: number;
912
json?: any;
1013
} = {}) {
1114
await TestBed.configureTestingModule({
@@ -14,9 +17,11 @@ async function setupTest({
1417
}).compileComponents();
1518

1619
const fixture = TestBed.createComponent(NgxJsonTreeviewComponent);
17-
const component = fixture.componentInstance;
20+
fixture.componentRef.setInput('depth', depth);
1821
fixture.componentRef.setInput('json', json);
1922
await fixture.whenStable();
23+
24+
const component = fixture.componentInstance;
2025
const loader = TestbedHarnessEnvironment.loader(fixture);
2126

2227
return { component, fixture, loader };
@@ -27,4 +32,28 @@ describe('NgxJsonTreeviewComponent', () => {
2732
const { component } = await setupTest();
2833
expect(component).toBeTruthy();
2934
});
35+
36+
it('should reset expandedSegments when depth changes', async () => {
37+
const { fixture, loader } = await setupTest({
38+
depth: 2,
39+
json: { nested: { deep: { inner: 1 } } },
40+
});
41+
42+
const deepNode = await loader.getHarness(
43+
NgxJsonTreeviewNodeHarness.with({ key: 'deep' })
44+
);
45+
expect(await deepNode.isExpanded()).toBe(true);
46+
47+
await deepNode.collapse();
48+
49+
expect(await deepNode.isExpanded()).toBe(false);
50+
51+
fixture.componentRef.setInput('depth', 3);
52+
await fixture.whenStable();
53+
54+
const deepNodeAfter = await loader.getHarness(
55+
NgxJsonTreeviewNodeHarness.with({ key: 'deep' })
56+
);
57+
expect(await deepNodeAfter.isExpanded()).toBe(true);
58+
});
3059
});

projects/ngx-json-treeview/src/lib/ngx-json-treeview/ngx-json-treeview.component.ts

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class NgxJsonTreeviewComponent {
3030
* The JSON object or array to display in the tree view.
3131
* @required
3232
*/
33-
json = input.required<any>();
33+
readonly json = input.required<any>();
3434

3535
/**
3636
* Controls the default expansion state for all expandable segments
@@ -39,7 +39,7 @@ export class NgxJsonTreeviewComponent {
3939
* - If `false`, all nodes start collapsed.
4040
* @default true
4141
*/
42-
expanded = input<boolean>(true);
42+
readonly expanded = input<boolean>(true);
4343

4444
/**
4545
* Determines the maximum nesting level automatically expanded when `expanded`
@@ -49,7 +49,7 @@ export class NgxJsonTreeviewComponent {
4949
* - `n`: Root and nodes down to `n` levels are expanded.
5050
* @default -1
5151
*/
52-
depth = input<number>(-1);
52+
readonly depth = input<number>(-1);
5353

5454
/**
5555
* If `true`, values are clickable when there is a corresponding handler
@@ -61,7 +61,7 @@ export class NgxJsonTreeviewComponent {
6161
* - Triggering custom actions based on the value's content or type.
6262
* @default false
6363
*/
64-
enableClickableValues = input<boolean>(false);
64+
readonly enableClickableValues = input<boolean>(false);
6565

6666
/**
6767
* A flag to control whether click events on nodes propagate up the DOM tree.
@@ -74,7 +74,7 @@ export class NgxJsonTreeviewComponent {
7474
*
7575
* @default true
7676
*/
77-
stopClickPropagation = input<boolean>(true);
77+
readonly stopClickPropagation = input<boolean>(true);
7878

7979
/**
8080
* @deprecated Use `valueClickHandlers` instead. This input will be removed
@@ -92,7 +92,7 @@ export class NgxJsonTreeviewComponent {
9292
* @returns `true` if the segment's value should be clickable, `false`
9393
* otherwise.
9494
*/
95-
isClickableValue = input<IsClickableValueFn>();
95+
readonly isClickableValue = input<IsClickableValueFn>();
9696

9797
/**
9898
* @deprecated Use `valueClickHandlers` instead. This output will be removed
@@ -102,7 +102,7 @@ export class NgxJsonTreeviewComponent {
102102
* a value node is clicked. The emitted `Segment` contains details about the
103103
* clicked node (key, value, type, path, etc.).
104104
*/
105-
onValueClick = output<Segment>();
105+
readonly onValueClick = output<Segment>();
106106

107107
/**
108108
* An array of handler functions to be executed when a value node is clicked.
@@ -112,28 +112,21 @@ export class NgxJsonTreeviewComponent {
112112
* If `enableClickableValues` is set to true, but `valueClickHandlers` is
113113
* omitted, the built-in `VALUE_CLICK_HANDLERS` will be used as the default.
114114
*/
115-
valueClickHandlers = input<ValueClickHandler[]>();
115+
readonly valueClickHandlers = input<ValueClickHandler[]>();
116116

117117
/**
118118
* *Internal* input representing the parent segment in the tree hierarchy.
119119
* Primrily used for calculating paths.
120120
* @internal
121121
*/
122-
_parent = input<Segment>();
122+
protected readonly _parent = input<Segment>();
123123

124124
/**
125125
* *Internal* input representing the current nesting depth. Used in
126126
* conjunction with the `depth` input to control expansion.
127127
* @internal
128128
*/
129-
_currentDepth = input<number>(0);
130-
131-
constructor() {
132-
effect(() => {
133-
this.depth();
134-
this.expandedSegments.set(new Map());
135-
});
136-
}
129+
protected readonly _currentDepth = input<number>(0);
137130

138131
private internalValueClickHandlers = computed<ValueClickHandler[]>(() => {
139132
const handlers: ValueClickHandler[] = [];
@@ -150,46 +143,53 @@ export class NgxJsonTreeviewComponent {
150143
return handlers;
151144
});
152145

153-
rootType = computed<string>(() => {
146+
private readonly rootType = computed<string>(() => {
154147
if (this.json() === null) {
155148
return 'null';
156149
} else if (Array.isArray(this.json())) {
157150
return 'array';
158151
} else return typeof this.json();
159152
});
160-
segments = computed<Segment[]>(() => {
153+
154+
protected readonly segments = computed<Segment[]>(() => {
161155
const json = decycle(this.json());
162156
if (typeof json === 'object' && json != null) {
163157
return Object.keys(json).map((key) => this.parseKeyValue(key, json[key]));
164158
}
165159
return [];
166160
});
167-
isExpanded = computed<boolean>(
161+
162+
private readonly isExpanded = computed<boolean>(
168163
() =>
169164
this.expanded() &&
170165
!(this.depth() > -1 && this._currentDepth() >= this.depth())
171166
);
172-
openingBrace = computed<string>(() => {
167+
168+
protected readonly openingBrace = computed<string>(() => {
173169
if (this.rootType() === 'array') {
174170
return '[';
175171
} else return '{';
176172
});
177-
closingBrace = computed<string>(() => {
173+
174+
protected readonly closingBrace = computed<string>(() => {
178175
if (this.rootType() === 'array') {
179176
return ']';
180177
} else return '}';
181178
});
182-
asString = computed<string>(() =>
179+
180+
protected readonly asString = computed<string>(() =>
183181
JSON.stringify(this.json(), null, 2).trim()
184182
);
185-
primitiveSegmentClass = computed<string>(() => {
183+
184+
protected readonly primitiveSegmentClass = computed<string>(() => {
186185
const type = this.rootType();
187186
if (['object', 'array'].includes(type)) {
188187
return 'punctuation';
189188
}
190189
return 'segment-type-' + type;
191190
});
192-
private primitiveSegment = computed<Segment | null>(() => {
191+
192+
private readonly primitiveSegment = computed<Segment | null>(() => {
193193
if (this.segments().length > 0) return null;
194194
return {
195195
key: '',
@@ -200,36 +200,47 @@ export class NgxJsonTreeviewComponent {
200200
path: this._parent()?.path ?? '',
201201
};
202202
});
203-
isClickablePrimitive = computed<boolean>(() => {
203+
204+
protected readonly isClickablePrimitive = computed<boolean>(() => {
204205
const segment = this.primitiveSegment();
205206
return !!segment && this.isClickable(segment);
206207
});
207-
isArrayElement = computed<boolean>(() => this.rootType() === 'array');
208+
209+
protected readonly isArrayElement = computed<boolean>(
210+
() => this.rootType() === 'array'
211+
);
208212

209213
/**
210214
* Tracks the expansion state of individual segments. Ensures user-toggled
211215
* states persist even when the underlying data or segments are re-generated.
212216
*/
213-
expandedSegments = signal<Map<string, boolean>>(new Map());
217+
private readonly expandedSegments = signal<Map<string, boolean>>(new Map());
214218

215219
private readonly idGenerator = inject(ID_GENERATOR);
216-
public readonly id = this.idGenerator.next();
220+
protected readonly id = this.idGenerator.next();
221+
222+
constructor() {
223+
effect(() => {
224+
this.depth();
225+
this.expandedSegments.set(new Map());
226+
});
227+
}
217228

218-
isExpandable(segment: Segment) {
229+
protected isExpandable(segment: Segment) {
219230
return (
220231
(segment.type === 'object' && Object.keys(segment.value).length > 0) ||
221232
(segment.type === 'array' && segment.value.length > 0)
222233
);
223234
}
224235

225-
isEmpty(segment: Segment) {
236+
protected isEmpty(segment: Segment) {
226237
return (
227238
(segment.type === 'object' && Object.keys(segment.value).length === 0) ||
228239
(segment.type === 'array' && segment.value.length === 0)
229240
);
230241
}
231242

232-
isClickable(segment: Segment): boolean {
243+
protected isClickable(segment: Segment): boolean {
233244
if (!this.enableClickableValues()) {
234245
return false;
235246
}
@@ -243,7 +254,7 @@ export class NgxJsonTreeviewComponent {
243254
});
244255
}
245256

246-
toggle(segment: Segment) {
257+
protected toggle(segment: Segment) {
247258
if (this.isExpandable(segment)) {
248259
this.expandedSegments.update((map) => {
249260
const newMap = new Map(map);
@@ -253,14 +264,14 @@ export class NgxJsonTreeviewComponent {
253264
}
254265
}
255266

256-
onPrimitiveClick(event?: MouseEvent): void {
267+
protected onPrimitiveClick(event?: MouseEvent): void {
257268
const segment = this.primitiveSegment();
258269
if (segment) {
259270
this.onValueClickHandler(segment, event);
260271
}
261272
}
262273

263-
onValueClickHandler(segment: Segment, event?: MouseEvent) {
274+
protected onValueClickHandler(segment: Segment, event?: MouseEvent) {
264275
for (const handler of this.internalValueClickHandlers()) {
265276
try {
266277
if (handler.canHandle(segment)) {
@@ -277,7 +288,7 @@ export class NgxJsonTreeviewComponent {
277288
}
278289
}
279290

280-
openingBraceForSegment(segment: Segment) {
291+
protected openingBraceForSegment(segment: Segment) {
281292
if (segment.type === 'array') {
282293
return '[';
283294
} else if (segment.type === 'object') {
@@ -287,7 +298,7 @@ export class NgxJsonTreeviewComponent {
287298
return undefined;
288299
}
289300

290-
closingBraceForSegment(segment: Segment) {
301+
protected closingBraceForSegment(segment: Segment) {
291302
if (segment.type === 'array') {
292303
return ']';
293304
} else if (segment.type === 'object') {

0 commit comments

Comments
 (0)