Skip to content

Commit 37ad724

Browse files
mariohmolclaude
andcommitted
merge: resolve conflict with master destroy() null-guard
Keep master's defensive if (this.editor) guard in destroy(), and retain the ngOnDestroy timer cleanup and onWindowResize handler from this branch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2 parents 7fe8eb4 + 6c1afe0 commit 37ad724

2 files changed

Lines changed: 165 additions & 8 deletions

File tree

Lines changed: 159 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
1-
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
1+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2+
import * as JSGantt from 'jsgantt-improved';
23

34
import { GanttEditorComponent } from './gantt.component';
45

5-
describe('GantteditorComponent', () => {
6+
describe('GanttEditorComponent', () => {
67
let component: GanttEditorComponent;
78
let fixture: ComponentFixture<GanttEditorComponent>;
9+
let mockEditorInstance: any;
810

9-
beforeEach(async(() => {
11+
function buildMockEditor(overrides: any = {}): any {
12+
return {
13+
getDivId: jasmine.createSpy('getDivId').and.returnValue('mock-div-id'),
14+
setOptions: jasmine.createSpy('setOptions'),
15+
AddTaskItemObject: jasmine.createSpy('AddTaskItemObject'),
16+
Draw: jasmine.createSpy('Draw'),
17+
options: {},
18+
...overrides
19+
};
20+
}
21+
22+
beforeEach(waitForAsync(() => {
1023
TestBed.configureTestingModule({
11-
declarations: [ GanttEditorComponent ]
12-
})
13-
.compileComponents();
24+
declarations: [GanttEditorComponent]
25+
}).compileComponents();
1426
}));
1527

1628
beforeEach(() => {
29+
mockEditorInstance = buildMockEditor();
30+
spyOn(JSGantt as any, 'GanttChart').and.returnValue(mockEditorInstance);
31+
1732
fixture = TestBed.createComponent(GanttEditorComponent);
1833
component = fixture.componentInstance;
1934
fixture.detectChanges();
@@ -22,4 +37,142 @@ describe('GantteditorComponent', () => {
2237
it('should be created', () => {
2338
expect(component).toBeTruthy();
2439
});
40+
41+
// ---------------------------------------------------------------------------
42+
// destroy()
43+
// ---------------------------------------------------------------------------
44+
45+
describe('destroy()', () => {
46+
it('should clear the container innerHTML when an editor exists', () => {
47+
const container = component.ganttEditorContainer.nativeElement;
48+
container.innerHTML = '<div>stale chart content</div>';
49+
50+
component.destroy();
51+
52+
expect(container.innerHTML).toBe('');
53+
});
54+
55+
it('should null out the editor reference when an editor exists', () => {
56+
expect(component.getEditor()).not.toBeNull();
57+
58+
component.destroy();
59+
60+
expect(component.getEditor()).toBeNull();
61+
});
62+
63+
it('should be a no-op (not throw) when editor is already null', () => {
64+
component.destroy(); // first call nulls the editor
65+
expect(() => component.destroy()).not.toThrow();
66+
});
67+
68+
it('should not clear the container when editor is null', () => {
69+
const container = component.ganttEditorContainer.nativeElement;
70+
container.innerHTML = '<div>existing content</div>';
71+
component.destroy(); // null the editor
72+
73+
component.destroy(); // second call — should leave the DOM untouched
74+
expect(container.innerHTML).toBe('<div>existing content</div>');
75+
});
76+
});
77+
78+
// ---------------------------------------------------------------------------
79+
// data setter
80+
// ---------------------------------------------------------------------------
81+
82+
describe('data setter', () => {
83+
it('should store the supplied value', () => {
84+
const rows = [{ pID: 1, pName: 'Task A' }];
85+
component.data = rows;
86+
// Access via getEditor flow: the data should have been passed to AddTaskItemObject
87+
expect(mockEditorInstance.AddTaskItemObject).toHaveBeenCalled();
88+
});
89+
90+
it('should call destroy() then re-initialise when an editor already exists', () => {
91+
const destroySpy = spyOn(component, 'destroy').and.callThrough();
92+
const initSpy = spyOn(component, 'ngOnInit').and.callThrough();
93+
94+
component.data = [{ pID: 2, pName: 'Task B' }];
95+
96+
expect(destroySpy).toHaveBeenCalledTimes(1);
97+
expect(initSpy).toHaveBeenCalledTimes(1);
98+
});
99+
100+
it('should still call ngOnInit when no prior editor exists but container is available', () => {
101+
// Reset so there is no editor
102+
component.destroy();
103+
(JSGantt as any).GanttChart.calls.reset();
104+
105+
const initSpy = spyOn(component, 'ngOnInit').and.callThrough();
106+
component.data = [{ pID: 3, pName: 'Task C' }];
107+
108+
expect(initSpy).toHaveBeenCalledTimes(1);
109+
});
110+
111+
it('should not call ngOnInit when ganttEditorContainer is not yet available', () => {
112+
// Simulate the case where the setter fires before the view is initialised
113+
(component as any).ganttEditorContainer = undefined;
114+
component.destroy(); // clear editor so we test the no-editor branch
115+
116+
const initSpy = spyOn(component, 'ngOnInit').and.callThrough();
117+
component.data = [{ pID: 4, pName: 'Task D' }];
118+
119+
expect(initSpy).not.toHaveBeenCalled();
120+
});
121+
122+
it('should create a fresh GanttChart instance on each data update', () => {
123+
const callsBefore = (JSGantt as any).GanttChart.calls.count();
124+
component.data = [{ pID: 5, pName: 'Task E' }];
125+
const callsAfter = (JSGantt as any).GanttChart.calls.count();
126+
127+
expect(callsAfter).toBe(callsBefore + 1);
128+
});
129+
130+
it('should register each data row with AddTaskItemObject', () => {
131+
const rows = [
132+
{ pID: 10, pName: 'Task 1' },
133+
{ pID: 11, pName: 'Task 2' },
134+
{ pID: 12, pName: 'Task 3' }
135+
];
136+
137+
component.data = rows;
138+
139+
expect(mockEditorInstance.AddTaskItemObject).toHaveBeenCalledTimes(rows.length);
140+
});
141+
142+
it('should set pGantt on each row to the new editor instance', () => {
143+
const rows = [{ pID: 20, pName: 'Task X' }];
144+
component.data = rows;
145+
146+
expect(rows[0].pGantt).toBe(component.getEditor());
147+
});
148+
});
149+
150+
// ---------------------------------------------------------------------------
151+
// destroy() + data setter interaction
152+
// ---------------------------------------------------------------------------
153+
154+
describe('destroy() then data setter interaction', () => {
155+
it('should leave the container clean before re-rendering', () => {
156+
const container = component.ganttEditorContainer.nativeElement;
157+
// Simulate stale content from a previous render
158+
container.innerHTML = '<div id="stale">old chart</div>';
159+
160+
component.data = [{ pID: 30, pName: 'Fresh Task' }];
161+
162+
// After destroy clears innerHTML and GanttChart re-renders, the stale
163+
// content should be gone (GanttChart writes its own output)
164+
expect(container.querySelector('#stale')).toBeNull();
165+
});
166+
167+
it('should expose the new editor via getEditor() after re-render', () => {
168+
const firstEditor = component.getEditor();
169+
const newMock = buildMockEditor();
170+
(JSGantt as any).GanttChart.and.returnValue(newMock);
171+
172+
component.data = [{ pID: 40, pName: 'New Task' }];
173+
174+
expect(component.getEditor()).toBe(newMock);
175+
expect(component.getEditor()).not.toBe(firstEditor);
176+
});
177+
});
25178
});

ng-gantt/src/gantt/gantt.component.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export class GanttEditorComponent implements OnInit, OnDestroy {
3030
this._data = value;
3131
if (this.editor) {
3232
this.destroy();
33+
}
34+
if (this.ganttEditorContainer) {
3335
this.ngOnInit();
3436
}
3537
}
@@ -88,8 +90,10 @@ export class GanttEditorComponent implements OnInit, OnDestroy {
8890
}
8991

9092
public destroy() {
91-
this.ganttEditorContainer.nativeElement.innerHTML = '';
92-
this.editor = null;
93+
if (this.editor) {
94+
this.ganttEditorContainer.nativeElement.innerHTML = '';
95+
this.editor = null;
96+
}
9397
}
9498

9599
ngOnDestroy() {

0 commit comments

Comments
 (0)